Merge branch 'develop' into data-options-child-table

This commit is contained in:
Suraj Shetty 2020-04-27 11:06:35 +05:30 committed by GitHub
commit 65c29161b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
108 changed files with 1678 additions and 1012 deletions

View file

@ -78,6 +78,7 @@
"has_common": true,
"has_words": true,
"validate_email": true,
"validate_name": true,
"validate_phone": true,
"get_number_format": true,
"format_number": true,

View file

@ -7,14 +7,19 @@ addons:
- test_site_producer
mariadb: 10.3
postgresql: 9.5
chrome: stable
git:
depth: 1
cache:
- pip
- npm
- yarn
pip: true
npm: true
yarn: true
directories:
# we also need to cache folder with Cypress binary
# https://docs.cypress.io/guides/guides/continuous-integration.html#Caching
- ~/.cache
matrix:
include:

View file

@ -1,8 +1,4 @@
context('Depends On', () => {
beforeEach(() => {
cy.login();
return cy.new_form('Test Depends On');
});
before(() => {
cy.login();
cy.visit('/desk#workspace/Website');

View file

@ -50,7 +50,7 @@ context('FileUploader', () => {
open_upload_dialog();
cy.get_open_dialog().find('a:contains("web link")').click();
cy.get_open_dialog().find('.file-web-link input').type('https://github.com');
cy.get_open_dialog().find('.file-web-link input').type('https://github.com', { delay: 100, force: true });
cy.server();
cy.route('POST', '/api/method/upload_file').as('upload_file');
cy.get_open_dialog().find('.btn-primary').click();

View file

@ -6,14 +6,17 @@ context('Form', () => {
return frappe.call("frappe.tests.ui_test_helpers.create_contact_records");
});
});
beforeEach(() => {
cy.visit('/desk#workspace/Website');
});
it('create a new form', () => {
cy.visit('/desk#Form/ToDo/New ToDo 1');
cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
cy.get('.page-title').should('contain', 'Not Saved');
cy.server();
cy.route({
method: 'POST',
url: 'api/method/frappe.desk.form.save.savedocs'
}).as('form_save');
cy.get('.primary-action').click();
cy.wait('@form_save').its('status').should('eq', 200);
cy.visit('/desk#List/ToDo');
cy.location('hash').should('eq', '#List/ToDo/List');
cy.get('h1').should('be.visible').and('contain', 'To Do');

View file

@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
if (fieldtype === 'Select') {
cy.get('@input').select(value);
} else {
cy.get('@input').type(value, { waitForAnimations: false });
cy.get('@input').type(value, { waitForAnimations: false, force: true });
}
return cy.get('@input');
});

View file

@ -219,7 +219,10 @@ class LoginManager:
user = frappe.db.get_value("User", filters={"username": user}, fieldname="name") or user
self.check_if_enabled(user)
self.user = self.check_password(user, pwd)
if not frappe.form_dict.get('tmp_id'):
self.user = self.check_password(user, pwd)
else:
self.user = user
def force_user_to_reset_password(self):
if not self.user:

View file

@ -3,7 +3,7 @@
{
"hidden": 0,
"label": "Tools",
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]"
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]"
},
{
"hidden": 0,
@ -32,7 +32,7 @@
"idx": 0,
"is_standard": 1,
"label": "Tools",
"modified": "2020-04-01 11:24:40.804346",
"modified": "2020-04-20 18:21:14.152537",
"modified_by": "Administrator",
"module": "Automation",
"name": "Tools",

View file

@ -17,6 +17,7 @@ from frappe.utils.change_log import get_versions
from frappe.translate import get_lang_dict
from frappe.email.inbox import get_email_accounts
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
from frappe.social.doctype.post.post import frequently_visited_links
@ -79,6 +80,7 @@ def get_bootinfo():
bootinfo.success_action = get_success_action()
bootinfo.update(get_email_accounts(user=frappe.session.user))
bootinfo.energy_points_enabled = is_energy_point_enabled()
bootinfo.website_tracking_enabled = is_tracking_enabled()
bootinfo.points = get_energy_points(frappe.session.user)
bootinfo.frequently_visited_links = frequently_visited_links()
bootinfo.link_preview_doctypes = get_link_preview_doctypes()
@ -268,4 +270,18 @@ def get_success_action():
return frappe.get_all("Success Action", fields=["*"])
def get_link_preview_doctypes():
return [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})]
from frappe.utils import cint
link_preview_doctypes = [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})]
customizations = frappe.get_all("Property Setter",
fields=['doc_type', 'value'],
filters={'property': 'show_preview_popup'}
)
for custom in customizations:
if not cint(custom.value) and custom.doc_type in link_preview_doctypes:
link_preview_doctypes.remove(custom.doc_type)
else:
link_preview_doctypes.append(custom.doc_type)
return link_preview_doctypes

View file

@ -522,7 +522,7 @@ def run_ui_tests(context, app, headless=False):
password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else ''
# run for headless mode
run_or_open = 'run --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
run_or_open = 'run --browser chrome --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
command = '{site_env} {password_env} yarn run cypress {run_or_open}'
formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open)
frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)

View file

@ -20,7 +20,7 @@ class TestExporter(unittest.TestCase):
e = Exporter('Web Page', export_fields='All')
csv_array = e.get_csv_array()
header = csv_array[0]
self.assertEqual(len(header), 24)
self.assertEqual(len(header), 28)
def test_exports_selected_fields(self):

View file

@ -11,9 +11,9 @@
"label",
"fieldtype",
"fieldname",
"reqd",
"precision",
"length",
"reqd",
"search_index",
"in_list_view",
"in_standard_filter",
@ -453,7 +453,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-04-15 02:26:03.310781",
"modified": "2020-04-19 21:54:13.783908",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -477,7 +477,8 @@ class DocType(Document):
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict['fields']))
if field_dict:
new_field_dicts.append(field_dict[0])
remaining_field_names.remove(fieldname)
if fieldname in remaining_field_names:
remaining_field_names.remove(fieldname)
for fieldname in remaining_field_names:
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict['fields']))
@ -498,7 +499,8 @@ class DocType(Document):
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict.get('fields', [])))
if field_dict:
new_field_dicts.append(field_dict[0])
remaining_field_names.remove(fieldname)
if fieldname in remaining_field_names:
remaining_field_names.remove(fieldname)
for fieldname in remaining_field_names:
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict.get('fields', [])))
@ -893,7 +895,7 @@ def validate_fields(meta):
field.fetch_from = field.fetch_from.strip('\n').strip()
def validate_data_field_type(docfield):
if docfield.fieldtype == "Data":
if docfield.fieldtype == "Data" and not (docfield.oldfieldtype and docfield.oldfieldtype != "Data"):
if docfield.options and (docfield.options not in data_field_options):
df_str = frappe.bold(_(docfield.label))
text_str = _("{0} is an invalid Data field.").format(df_str) + "<br>" * 2 + _("Only Options allowed for Data field are:") + "<br>"

View file

@ -1,46 +1,45 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
from frappe import _
"""
record of files
naming for same name files: file.gif, file-1.gif, file-2.gif etc
"""
import frappe
import json
import os
from __future__ import unicode_literals
import base64
import re
import hashlib
import mimetypes
import imghdr
import io
import json
import mimetypes
import os
import re
import shutil
import zipfile
import requests
import requests.exceptions
import imghdr
from PIL import Image, ImageFile, ImageOps
from six import PY2, StringIO, string_types, text_type
from six.moves.urllib.parse import quote, unquote
from frappe.utils import get_hook_method, get_files_path, random_string, encode, cstr, call_hook_method, cint
from frappe import _
from frappe import conf
from frappe.utils.nestedset import NestedSet
import frappe
from frappe import _, conf
from frappe.model.document import Document
from frappe.utils import strip
from PIL import Image, ImageOps
from six import StringIO, string_types
from six.moves.urllib.parse import unquote, quote
from six import text_type, PY2
import zipfile
from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, get_hook_method, random_string, strip
class MaxFileSizeReachedError(frappe.ValidationError):
pass
class FolderNotEmpty(frappe.ValidationError): pass
class FolderNotEmpty(frappe.ValidationError):
pass
exclude_from_linked_with = True
ImageFile.LOAD_TRUNCATED_IMAGES = True
class File(Document):
@ -697,7 +696,7 @@ def remove_file(fid=None, attached_to_doctype=None, attached_to_name=None, from_
def get_max_file_size():
return conf.get('max_file_size') or 10485760
return cint(conf.get('max_file_size')) or 10485760
def remove_all(dt, dn, from_delete=False):
@ -714,7 +713,10 @@ def has_permission(doc, ptype=None, user=None):
has_access = False
user = user or frappe.session.user
if not doc.is_private or doc.owner == user or user == 'Administrator':
if ptype == 'create':
has_access = frappe.has_permission('File', 'create', user=user)
if not doc.is_private or doc.owner in [user, 'Guest'] or user == 'Administrator':
has_access = True
if doc.attached_to_doctype and doc.attached_to_name:

View file

@ -97,47 +97,49 @@ frappe.ui.form.on('User', {
});
}, __("Password"));
frappe.db.get_single_value("LDAP Settings", "enabled").then((value) => {
if (value === 1 && frm.doc.name != "Administrator") {
frm.add_custom_button(__("Reset LDAP Password"), function() {
const d = new frappe.ui.Dialog({
title: __("Reset LDAP Password"),
fields: [
{
label: __("New Password"),
fieldtype: "Password",
fieldname: "new_password",
reqd: 1
},
{
label: __("Confirm New Password"),
fieldtype: "Password",
fieldname: "confirm_password",
reqd: 1
},
{
label: __("Logout All Sessions"),
fieldtype: "Check",
fieldname: "logout_sessions"
if (frappe.user.has_role("System Manager")) {
frappe.db.get_single_value("LDAP Settings", "enabled").then((value) => {
if (value === 1 && frm.doc.name != "Administrator") {
frm.add_custom_button(__("Reset LDAP Password"), function() {
const d = new frappe.ui.Dialog({
title: __("Reset LDAP Password"),
fields: [
{
label: __("New Password"),
fieldtype: "Password",
fieldname: "new_password",
reqd: 1
},
{
label: __("Confirm New Password"),
fieldtype: "Password",
fieldname: "confirm_password",
reqd: 1
},
{
label: __("Logout All Sessions"),
fieldtype: "Check",
fieldname: "logout_sessions"
}
],
primary_action: (values) => {
d.hide();
if (values.new_password !== values.confirm_password) {
frappe.throw(__("Passwords do not match!"));
}
frappe.call(
"frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", {
user: frm.doc.email,
password: values.new_password,
logout: values.logout_sessions
});
}
],
primary_action: (values) => {
d.hide();
if (values.new_password !== values.confirm_password) {
frappe.throw(__("Passwords do not match!"));
}
frappe.call(
"frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", {
user: frm.doc.email,
password: values.new_password,
logout: values.logout_sessions
});
}
});
d.show();
}, __("Password"));
}
});
});
d.show();
}, __("Password"));
}
});
}
frm.add_custom_button(__("Reset OTP Secret"), function() {
frappe.call({

View file

@ -551,6 +551,7 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password=
res = _get_user_for_update_password(key, old_password)
if res.get('message'):
frappe.local.response.http_status_code = 410
return res['message']
else:
user = res['user']
@ -718,7 +719,7 @@ def _get_user_for_update_password(key, old_password):
user = frappe.db.get_value("User", {"reset_password_key": key})
if not user:
return {
'message': _("Cannot Update: Incorrect / Expired Link.")
'message': _("The Link specified has either been used before or Invalid")
}
elif old_password:

View file

@ -9,7 +9,8 @@ import unittest
class TestUserPermission(unittest.TestCase):
def setUp(self):
frappe.db.sql("DELETE FROM `tabUser Permission` WHERE `user`='test_bulk_creation_update@example.com'")
frappe.db.sql("""DELETE FROM `tabUser Permission`
WHERE `user` in ('test_bulk_creation_update@example.com', 'test_user_perm1@example.com')""")
def test_default_user_permission_validation(self):
user = create_user('test_default_permission@example.com')
@ -20,6 +21,26 @@ class TestUserPermission(unittest.TestCase):
param = get_params(user, 'User', perm_user.name, is_default=1)
self.assertRaises(frappe.ValidationError, add_user_permissions, param)
def test_default_user_permission(self):
frappe.set_user('Administrator')
user = create_user('test_user_perm1@example.com', 'Website Manager')
for category in ['general', 'public']:
if not frappe.db.exists('Blog Category', category):
frappe.get_doc({'doctype': 'Blog Category',
'category_name': category, 'title': category}).insert()
param = get_params(user, 'Blog Category', 'general', is_default=1)
add_user_permissions(param)
param = get_params(user, 'Blog Category', 'public')
add_user_permissions(param)
frappe.set_user('test_user_perm1@example.com')
doc = frappe.new_doc("Blog Post")
self.assertEquals(doc.blog_category, 'general')
frappe.set_user('Administrator')
def test_apply_to_all(self):
''' Create User permission for User having access to all applicable Doctypes'''
user = create_user('test_bulk_creation_update@example.com')
@ -88,7 +109,7 @@ class TestUserPermission(unittest.TestCase):
self.assertIsNone(removed_applicable_second)
self.assertEquals(is_created, 1)
def create_user(email):
def create_user(email, role="System Manager"):
''' create user with role system manager '''
if frappe.db.exists('User', email):
return frappe.get_doc('User', email)
@ -96,7 +117,7 @@ def create_user(email):
user = frappe.new_doc('User')
user.email = email
user.first_name = email.split("@")[0]
user.add_roles("System Manager")
user.add_roles(role)
return user
def get_params(user, doctype, docname, is_default=0, applicable=None):

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestVideo(unittest.TestCase):
pass

View file

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

View file

@ -0,0 +1,106 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:title",
"creation": "2018-10-17 05:47:13.087395",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"provider",
"url",
"column_break_4",
"publish_date",
"duration",
"section_break_7",
"description"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1,
"unique": 1
},
{
"fieldname": "provider",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Provider",
"options": "YouTube\nVimeo",
"reqd": 1
},
{
"fieldname": "url",
"fieldtype": "Data",
"in_list_view": 1,
"label": "URL",
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "publish_date",
"fieldtype": "Date",
"label": "Publish Date"
},
{
"fieldname": "duration",
"fieldtype": "Data",
"label": "Duration"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
},
{
"fieldname": "description",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Description",
"reqd": 1
}
],
"links": [],
"modified": "2020-04-22 12:09:49.057403",
"modified_by": "Administrator",
"module": "Core",
"name": "Video",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class WebViewItem(Document):
class Video(Document):
pass

View file

@ -28,6 +28,7 @@ def get_info(show_failed=False):
if j.kwargs.get('site')==frappe.local.site:
jobs.append({
'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') \
or j.kwargs.get('kwargs', {}).get('job_type') \
or str(j.kwargs.get('job_name')),
'status': j.get_status(), 'queue': name,
'creation': format_datetime(convert_utc_to_user_timezone(j.created_at)),

View file

@ -41,6 +41,7 @@
"in_list_view",
"in_standard_filter",
"in_global_search",
"in_preview",
"bold",
"report_hide",
"search_index",
@ -371,12 +372,18 @@
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
},
{
"default": "0",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
}
],
"icon": "fa fa-glass",
"idx": 1,
"links": [],
"modified": "2020-03-16 14:52:43.954709",
"modified": "2020-04-10 11:57:10.392218",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",

View file

@ -20,6 +20,7 @@
"track_views",
"allow_auto_repeat",
"allow_import",
"show_preview_popup",
"image_view",
"column_break_5",
"title_field",
@ -203,6 +204,12 @@
"depends_on": "doc_type",
"fieldname": "section_break_23",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "show_preview_popup",
"fieldtype": "Check",
"label": "Show Preview Popup"
}
],
"hide_toolbar": 1,
@ -210,7 +217,7 @@
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2020-03-27 15:06:35.443861",
"modified": "2020-04-10 12:16:01.320411",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",

View file

@ -32,6 +32,7 @@ doctype_properties = {
'track_views': 'Check',
'allow_auto_repeat': 'Check',
'allow_import': 'Check',
'show_preview_popup': 'Check',
'email_append_to': 'Check',
'subject_field': 'Data',
'sender_field': 'Data'
@ -53,6 +54,7 @@ docfield_properties = {
'in_list_view': 'Check',
'in_standard_filter': 'Check',
'in_global_search': 'Check',
'in_preview': 'Check',
'bold': 'Check',
'hidden': 'Check',
'collapsible': 'Check',

View file

@ -16,6 +16,7 @@
"in_list_view",
"in_standard_filter",
"in_global_search",
"in_preview",
"bold",
"allow_in_quick_entry",
"translatable",
@ -381,12 +382,18 @@
"fieldtype": "Code",
"label": "Read Only Depends On",
"options": "JS"
},
{
"default": "0",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-04-15 02:26:59.673750",
"modified": "2020-04-10 11:58:44.573537",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",

View file

@ -59,6 +59,10 @@ frappe.ui.form.on('Dashboard Chart', {
if (frm.doc.report_name) {
frm.trigger('set_chart_report_filters');
}
if (!frappe.boot.developer_mode) {
frm.set_df_property("custom_options", "hidden", 1);
}
},
source: function(frm) {

View file

@ -33,6 +33,7 @@
"type",
"column_break_2",
"color",
"custom_options",
"section_break_10",
"last_synced_on"
],
@ -124,7 +125,7 @@
"fieldname": "type",
"fieldtype": "Select",
"label": "Type",
"options": "Line\nBar\nPercentage\nPie",
"options": "Line\nBar\nPercentage\nPie\nDonut",
"reqd": 1
},
{
@ -213,10 +214,16 @@
"label": "Y Axis",
"mandatory_depends_on": "eval:doc.report_name && !doc.is_custom",
"options": "Dashboard Chart Field"
},
{
"description": "Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"]",
"fieldname": "custom_options",
"fieldtype": "Code",
"label": "Custom Options"
}
],
"links": [],
"modified": "2020-04-08 18:54:36.739183",
"modified": "2020-04-20 23:49:11.389909",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",

View file

@ -79,7 +79,7 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
to_date = chart.to_date
timegrain = time_interval or chart.time_interval
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json)
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) or []
# don't include cancelled documents
filters.append([chart.document_type, 'docstatus', '<', 2, False])
@ -97,6 +97,10 @@ def create_report_chart(args):
_doc = frappe.new_doc('Dashboard Chart')
_doc.update(args)
if (args.get("custom_options")):
_doc.custom_options = json.dumps(args.get("custom_options"))
if frappe.db.exists('Dashboard Chart', args.chart_name):
args.chart_name = append_number_if_name_exists('Dashboard Chart', args.chart_name)
_doc.chart_name = args.chart_name
@ -108,6 +112,7 @@ def create_report_chart(args):
@frappe.whitelist()
def add_chart_to_dashboard(args):
args = frappe.parse_json(args)
dashboard = frappe.get_doc('Dashboard', args.dashboard)
dashboard_link = frappe.new_doc('Dashboard Chart Link')
dashboard_link.chart = args.chart_name
@ -362,6 +367,8 @@ class DashboardChart(Document):
self.check_required_field()
self.check_document_type()
self.validate_custom_options()
def check_required_field(self):
if not self.document_type:
frappe.throw(_("Document type is required to create a dashboard chart"))
@ -378,3 +385,10 @@ class DashboardChart(Document):
def check_document_type(self):
if frappe.get_meta(self.document_type).issingle:
frappe.throw("You cannot create a dashboard chart from single DocTypes")
def validate_custom_options(self):
if self.custom_options:
try:
json.loads(self.custom_options)
except ValueError as error:
frappe.throw("Invalid json added in the custom options: %s" % error)

View file

@ -196,8 +196,6 @@ class FormMeta(Meta):
self.get("__messages").update(messages, as_value=True)
def load_dashboard(self):
if self.custom:
return
self.set('__dashboard', self.get_dashboard_data())
def load_kanban_meta(self):

View file

@ -268,8 +268,9 @@ def get_open_count(doctype, name, items=[]):
"count": out,
}
module = frappe.get_meta_module(doctype)
if hasattr(module, "get_timeline_data"):
out["timeline_data"] = module.get_timeline_data(doctype, name)
if not meta.custom:
module = frappe.get_meta_module(doctype)
if hasattr(module, "get_timeline_data"):
out["timeline_data"] = module.get_timeline_data(doctype, name)
return out

View file

@ -242,7 +242,7 @@ def get_prepared_report_result(report, filters, dn="", user=None):
columns = json.loads(doc.columns) if doc.columns else data[0]
for column in columns:
if isinstance(column, dict):
if isinstance(column, dict) and column.get("label"):
column["label"] = _(column["label"])
latest_report_data = {
@ -299,6 +299,7 @@ def export_query():
_("You can try changing the filters of your report."))
return
data.columns = [col for col in data.columns if isinstance(col, dict) and not col.get('hidden')]
columns = get_columns_dict(data.columns)
from frappe.utils.xlsxutils import make_xlsx
@ -310,7 +311,7 @@ def export_query():
frappe.response['type'] = 'binary'
def build_xlsx_data(columns, data, visible_idx,include_indentation):
def build_xlsx_data(columns, data, visible_idx, include_indentation):
result = [[]]
# add column headings

View file

@ -39,7 +39,7 @@ class EmailDomain(Document):
except Exception:
frappe.throw(_("Incoming email account not correct"))
return None
finally:
try:
if self.use_imap:
@ -48,9 +48,10 @@ class EmailDomain(Document):
test.quit()
except Exception:
pass
try:
if self.use_ssl_for_outgoing:
if not self.smtp_port:
if self.get('use_ssl_for_outgoing'):
if not self.get('smtp_port'):
self.smtp_port = 465
sess = smtplib.SMTP_SSL((self.smtp_server or "").encode('utf-8'),
@ -62,28 +63,15 @@ class EmailDomain(Document):
sess.quit()
except Exception:
frappe.throw(_("Outgoing email account not correct"))
return None
return
def on_update(self):
"""update all email accounts using this domain"""
for email_account in frappe.get_all("Email Account",
filters={"domain": self.name}):
for email_account in frappe.get_all("Email Account", filters={"domain": self.name}):
try:
email_account = frappe.get_doc("Email Account",
email_account.name)
email_account.set("email_server",self.email_server)
email_account.set("use_imap",self.use_imap)
email_account.set("use_ssl",self.use_ssl)
email_account.set("use_tls",self.use_tls)
email_account.set("attachment_limit",self.attachment_limit)
email_account.set("smtp_server",self.smtp_server)
email_account.set("smtp_port",self.smtp_port)
email_account.set("use_ssl_for_outgoing", self.use_ssl_for_outgoing)
email_account.set("append_emails_to_sent_folder", self.append_emails_to_sent_folder)
email_account = frappe.get_doc("Email Account", email_account.name)
for attr in ["email_server", "use_imap", "use_ssl", "use_tls", "attachment_limit", "smtp_server", "smtp_port", "use_ssl_for_outgoing", "append_emails_to_sent_folder"]:
email_account.set(attr, self.get(attr, default=0))
email_account.save()
except Exception as e:
frappe.msgprint(email_account.name)
frappe.throw(e)
return None
frappe.msgprint(_("Error has occurred in {0}").format(email_account.name), raise_exception=e.__class__)

View file

@ -78,6 +78,7 @@ class TimestampMismatchError(ValidationError): pass
class EmptyTableError(ValidationError): pass
class LinkExistsError(ValidationError): pass
class InvalidEmailAddressError(ValidationError): pass
class InvalidNameError(ValidationError): pass
class InvalidPhoneNumberError(ValidationError): pass
class TemplateNotFoundError(ValidationError): pass
class UniqueValidationError(ValidationError): pass
@ -95,4 +96,4 @@ class DataTooLongException(ValidationError): pass
# OAuth exceptions
class InvalidAuthorizationHeader(CSRFTokenError): pass
class InvalidAuthorizationPrefix(CSRFTokenError): pass
class InvalidAuthorizationToken(CSRFTokenError): pass
class InvalidAuthorizationToken(CSRFTokenError): pass

View file

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _, safe_encode
from frappe.model.document import Document
from frappe.twofactor import (should_run_2fa, authenticate_for_2factor,confirm_otp_token)
class LDAPSettings(Document):
def validate(self):
@ -237,6 +237,10 @@ def login():
user = ldap.authenticate(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd))
frappe.local.login_manager.user = user.name
if should_run_2fa(user.name):
authenticate_for_2factor(user.name)
if not confirm_otp_token(frappe.local.login_manager):
return False
frappe.local.login_manager.post_login()
# because of a GET request!

View file

@ -48,7 +48,7 @@ table_fields = ('Table', 'Table MultiSelect')
core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link', 'User', 'Role', 'Has Role',
'Page', 'Module Def', 'Print Format', 'Report', 'Customize Form',
'Customize Form Field', 'Property Setter', 'Custom Field', 'Custom Script')
data_field_options = ('Email', 'Phone')
data_field_options = ('Email', 'Name', 'Phone')
def copytables(srctype, src, srcfield, tartype, tar, tarfield, srcfields, tarfields=[]):
if not tarfields:

View file

@ -11,11 +11,12 @@ from frappe.model import default_fields, table_fields
from frappe.model.naming import set_new_name
from frappe.model.utils.link_count import notify_link_count
from frappe.modules import load_doctype_module
from frappe.model import display_fieldtypes, data_fieldtypes
from frappe.model import display_fieldtypes
from frappe.utils.password import get_decrypted_password, set_encrypted_password
from frappe.utils import (cint, flt, now, cstr, strip_html, getdate, get_datetime, to_timedelta,
from frappe.utils import (cint, flt, now, cstr, strip_html,
sanitize_html, sanitize_email, cast_fieldtype)
from frappe.utils.html_utils import unescape_html
from bs4 import BeautifulSoup
max_positive_value = {
'smallint': 2 ** 15,
@ -288,7 +289,7 @@ class BaseDocument(object):
if k in default_fields:
del doc[k]
for key in ("_user_tags", "__islocal", "__onload", "_liked_by", "__run_link_triggers"):
for key in ("_user_tags", "__islocal", "__onload", "_liked_by", "__run_link_triggers", "__unsaved"):
if self.get(key):
doc[key] = self.get(key)
@ -564,13 +565,20 @@ class BaseDocument(object):
for data_field in self.meta.get_data_fields():
data = self.get(data_field.fieldname)
data_field_options = data_field.get("options")
old_fieldtype = data_field.get("oldfieldtype")
if old_fieldtype and old_fieldtype != "Data":
continue
if data_field_options == "Email":
if (self.owner in STANDARD_USERS) and (data in STANDARD_USERS):
return
continue
for email_address in frappe.utils.split_emails(data):
frappe.utils.validate_email_address(email_address, throw=True)
if data_field_options == "Name":
frappe.utils.validate_name(data, throw=True)
if data_field_options == "Phone":
frappe.utils.validate_phone_number(data, throw=True)
@ -678,7 +686,7 @@ class BaseDocument(object):
# doesn't look like html so no need
continue
elif "<!-- markdown -->" in value and not ("<script" in value or "javascript:" in value):
elif "<!-- markdown -->" in value and not bool(BeautifulSoup(value, "html.parser").find()):
# should be handled separately via the markdown converter function
continue

View file

@ -74,11 +74,9 @@ def set_user_and_static_default_values(doc):
def get_user_default_value(df, defaults, doctype_user_permissions, allowed_records, default_doc):
# don't set defaults for "User" link field using User Permissions!
if df.fieldtype == "Link" and df.options != "User":
# 1 - look in user permissions only for document_type==Setup
# We don't want to include permissions of transactions to be used for defaults.
if (frappe.get_meta(df.options).document_type=="Setup"
and not df.ignore_user_permissions and default_doc):
return default_doc
# If user permission has Is Default enabled or single-user permission has found against respective doctype.
if (not df.ignore_user_permissions and default_doc):
return default_doc
# 2 - Look in user defaults
user_default = defaults.get(df.fieldname)

View file

@ -268,6 +268,10 @@ class Document(BaseDocument):
if hasattr(self, "__islocal"):
delattr(self, "__islocal")
# clear unsaved flag
if hasattr(self, "__unsaved"):
delattr(self, "__unsaved")
if not (frappe.flags.in_migrate or frappe.local.flags.in_install or frappe.flags.in_setup_wizard):
follow_document(self.doctype, self.name, frappe.session.user)
return self
@ -329,6 +333,10 @@ class Document(BaseDocument):
self.update_children()
self.run_post_save_methods()
# clear unsaved flag
if hasattr(self, "__unsaved"):
delattr(self, "__unsaved")
return self
def copy_attachments_from_amended_from(self):
@ -583,6 +591,9 @@ class Document(BaseDocument):
if high_permlevel_fields:
self.reset_values_if_no_permlevel_access(has_access_to, high_permlevel_fields)
# If new record then don't reset the values for child table
if self.is_new(): return
# check for child tables
for df in self.meta.get_table_fields():
high_permlevel_fields = frappe.get_meta(df.options).get_high_permlevel_fields()
@ -1318,6 +1329,9 @@ def make_event_update_log(doc, update_type):
def check_doctype_has_consumers(doctype):
"""Check if doctype has event consumers for event streaming"""
if not frappe.db.exists("DocType", "Event Consumer"):
return False
event_consumers = frappe.get_all('Event Consumer')
for event_consumer in event_consumers:
consumer = frappe.get_doc('Event Consumer', event_consumer.name)

View file

@ -425,17 +425,19 @@ class Meta(Document):
implemented in other Frappe applications via hooks.
'''
data = frappe._dict()
try:
module = load_doctype_module(self.name, suffix='_dashboard')
if hasattr(module, 'get_data'):
data = frappe._dict(module.get_data())
except ImportError:
pass
if not self.custom:
try:
module = load_doctype_module(self.name, suffix='_dashboard')
if hasattr(module, 'get_data'):
data = frappe._dict(module.get_data())
except ImportError:
pass
self.add_doctype_links(data)
for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []):
data = frappe.get_attr(hook)(data=data)
if not self.custom:
for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []):
data = frappe.get_attr(hook)(data=data)
return data

View file

@ -1,6 +1,10 @@
import frappe
def execute():
frappe.reload_doc("contacts", "doctype", "contact_email")
frappe.reload_doc("contacts", "doctype", "contact_phone")
frappe.reload_doc("contacts", "doctype", "contact")
contact_details = frappe.db.sql("""
SELECT
`name`, `email_id`, `phone`, `mobile_no`, `modified_by`, `creation`, `modified`
@ -10,10 +14,6 @@ def execute():
and `tabContact Email`.email_id=`tabContact`.email_id)
""", as_dict=True)
frappe.reload_doc("contacts", "doctype", "contact_email")
frappe.reload_doc("contacts", "doctype", "contact_phone")
frappe.reload_doc("contacts", "doctype", "contact")
email_values = []
phone_values = []
for count, contact_detail in enumerate(contact_details):

View file

@ -441,18 +441,16 @@ frappe.PrintFormatBuilder = Class.extend({
});
},
setup_field_settings: function() {
this.page.main.find(".field-settings").on("click", () => {
var field = $(this).parent();
this.page.main.find(".field-settings").on("click", e => {
const field = $(e.currentTarget).parent();
// new dialog
var d = new frappe.ui.Dialog({
title: "Set Properties",
fields: [
{
label:__("Label"),
fieldname:"label",
fieldtype:"Data"
label: __("Label"),
fieldname: "label",
fieldtype: "Data"
},
{
label: __("Align Value"),
@ -485,7 +483,7 @@ frappe.PrintFormatBuilder = Class.extend({
});
// set current value
if(field.attr('data-align')) {
if (field.attr('data-align')) {
d.set_value('align', field.attr('data-align'));
} else {
d.set_value('align', 'left');

View file

@ -90,6 +90,7 @@
"public/css/font-awesome.css",
"public/css/octicons/octicons.css",
"public/less/desk.less",
"public/less/module.less",
"public/less/flex.less",
"public/less/indicator.less",
"public/less/avatar.less",

View file

@ -48,6 +48,7 @@ frappe.ui.form.ControlBarcode = frappe.ui.form.ControlData.extend({
const svg = this.barcode_area.find('svg')[0];
JsBarcode(svg, value, this.get_options(value));
$(svg).attr('data-barcode-value', value);
$(svg).attr('width', '100%');
return this.barcode_area.html();
}
},

View file

@ -9,6 +9,12 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
this.ace_editor_target = $('<div class="ace-editor-target"></div>')
.appendTo(this.input_area);
this.expanded = false;
this.$expand_button = $(`<button class="btn btn-xs btn-default">${__('Expand')}</button>`).click(() => {
this.expanded = !this.expanded;
this.refresh_height();
this.toggle_label();
}).appendTo(this.$input_wrapper);
// styling
this.ace_editor_target.addClass('border rounded');
this.ace_editor_target.css('height', 300);
@ -26,6 +32,16 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
}, 300));
},
refresh_height() {
this.ace_editor_target.css('height', this.expanded ? 600 : 300);
this.editor.resize();
},
toggle_label() {
const button_label = this.expanded ? __('Collapse') : __('Expand');
this.$expand_button.text(button_label);
},
set_language() {
const language_map = {
'Javascript': 'ace/mode/javascript',
@ -34,7 +50,9 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
'CSS': 'ace/mode/css',
'Markdown': 'ace/mode/markdown',
'SCSS': 'ace/mode/scss',
'JSON': 'ace/mode/json'
'JSON': 'ace/mode/json',
'Golang': 'ace/mode/golang',
'Go': 'ace/mode/golang'
};
const language = this.df.options;

View file

@ -96,6 +96,9 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({
if(this.df.options == 'Phone') {
this.df.invalid = !validate_phone(v);
return v;
} else if (this.df.options == 'Name') {
this.df.invalid = !validate_name(v);
return v;
} else if(this.df.options == 'Email') {
var email_list = frappe.utils.split_emails(v);
if (!email_list) {

View file

@ -18,6 +18,7 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({
this.$list_wrapper = $(template);
this.$input = $('<input>');
this.input = this.$input.get(0);
this.has_input = true;
this.$list_wrapper.prependTo(this.input_area);
this.$filter_input = this.$list_wrapper.find('input');
this.$list_wrapper.on('click', '.dropdown-menu', e => {

View file

@ -184,13 +184,7 @@ frappe.ui.form.Form = class FrappeForm {
frappe.model.on(me.doctype, "*", function(fieldname, value, doc) {
// set input
if(doc.name===me.docname) {
if ((value==='' || value===null) && !doc[fieldname]) {
// both the incoming and outgoing values are falsy
// the texteditor, summernote, changes nulls to empty strings on render,
// so ignore those changes
} else {
me.dirty();
}
me.dirty();
let field = me.fields_dict[fieldname];
field && field.refresh(fieldname);

View file

@ -22,9 +22,6 @@ export default class GridRow {
if(me.grid.allow_on_grid_editing() && me.grid.is_editable()) {
// pass
} else {
if (!me.grid.is_editable()) {
me.docfields.map(df => df.read_only = 1);
}
me.toggle_view();
return false;
}

View file

@ -69,7 +69,7 @@ frappe.ui.form.Sidebar = Class.extend({
},
refresh: function() {
if(this.frm.doc.__islocal) {
if (this.frm.doc.__islocal) {
this.sidebar.toggle(false);
} else {
this.sidebar.toggle(true);
@ -81,12 +81,34 @@ frappe.ui.form.Sidebar = Class.extend({
}
this.frm.viewers.refresh();
this.frm.tags && this.frm.tags.refresh(this.frm.get_docinfo().tags);
this.sidebar.find(".modified-by").html(__("{0} edited this {1}",
["<strong>" + frappe.user.full_name(this.frm.doc.modified_by) + "</strong>",
"<br>" + comment_when(this.frm.doc.modified)]));
this.sidebar.find(".created-by").html(__("{0} created this {1}",
["<strong>" + frappe.user.full_name(this.frm.doc.owner) + "</strong>",
"<br>" + comment_when(this.frm.doc.creation)]));
if (this.frm.doc.route && cint(frappe.boot.website_tracking_enabled)) {
let route = this.frm.doc.route;
frappe.utils.get_page_view_count(route).then((res) => {
this.sidebar
.find(".pageview-count")
.html(
__("{0} Page Views", [String(res.message).bold()])
);
});
}
this.sidebar
.find(".modified-by")
.html(
__("{0} edited this {1}", [
frappe.user.full_name(this.frm.doc.modified_by).bold(),
"<br>" + comment_when(this.frm.doc.modified),
])
);
this.sidebar
.find(".created-by")
.html(
__("{0} created this {1}", [
frappe.user.full_name(this.frm.doc.owner).bold(),
"<br>" + comment_when(this.frm.doc.creation),
])
);
this.refresh_like();
frappe.ui.form.set_user_image(this.frm);

View file

@ -105,6 +105,7 @@
</li>
</ul>
<ul class="list-unstyled sidebar-menu text-muted">
<li class="pageview-count"></li>
<li class="modified-by"></li>
<li class="created-by"></li>
</ul>

View file

@ -137,10 +137,8 @@ $.extend(frappe.model, {
// don't set defaults for "User" link field using User Permissions!
if (df.fieldtype==="Link" && df.options!=="User") {
// 1 - look in user permissions for document_type=="Setup".
// We don't want to include permissions of transactions to be used for defaults.
if (df.linked_document_type==="Setup"
&& has_user_permissions && default_doc) {
// If user permission has Is Default enabled or single-user permission has found against respective doctype.
if (has_user_permissions && default_doc) {
return default_doc;
}
@ -161,10 +159,6 @@ $.extend(frappe.model, {
user_default = frappe.boot.user.last_selected_values[df.options];
}
if (!user_default && default_doc) {
user_default = default_doc;
}
var is_allowed_user_default = user_default &&
(!has_user_permissions || allowed_records.includes(user_default));

View file

@ -352,3 +352,9 @@ frappe.utils.new_auto_repeat_prompt = function(frm) {
__('Save')
);
}
frappe.utils.get_page_view_count = function(route) {
return frappe.call("frappe.website.doctype.web_page_view.web_page_view.get_page_view_count", {
path: route
});
};

View file

@ -48,6 +48,10 @@ window.validate_phone = function(txt) {
return frappe.utils.validate_type(txt, "phone");
};
window.validate_name = function(txt) {
return frappe.utils.validate_type(txt, "name");
};
window.nth = function(number) {
number = cint(number);
var s = 'th';
@ -73,4 +77,4 @@ window.has_common = function(list1, list2) {
if(in_list(list2, list1[i]))return true;
}
return false;
};
};

View file

@ -13,7 +13,7 @@ function prettyDate(date, mini) {
// Return short format of time difference
if (day_diff == 0) {
if (diff < 60) {
return __("Now");
return __("now");
} else if (diff < 3600) {
return __("{0} m", [Math.floor(diff / 60)]);
} else if (diff < 86400) {
@ -21,20 +21,20 @@ function prettyDate(date, mini) {
}
} else {
if (day_diff < 7) {
return __("{0} D", [day_diff]);
return __("{0} d", [day_diff]);
} else if (day_diff < 31) {
return __("{0} W", [Math.ceil(day_diff / 7)]);
return __("{0} w", [Math.ceil(day_diff / 7)]);
} else if (day_diff < 365) {
return __("{0} M", [Math.ceil(day_diff / 30)]);
} else {
return __("{0} Y", [Math.ceil(day_diff / 365)]);
return __("{0} y", [Math.ceil(day_diff / 365)]);
}
}
} else {
// Return long format of time difference
if (day_diff == 0) {
if (diff < 60) {
return __("Just now");
return __("just now");
} else if (diff < 120) {
return __("1 minute ago");
} else if (diff < 3600) {
@ -46,7 +46,7 @@ function prettyDate(date, mini) {
}
} else {
if (day_diff == 1) {
return __("Yesterday");
return __("yesterday");
} else if (day_diff < 7) {
return __("{0} days ago", [day_diff]);
} else if (day_diff < 14) {

View file

@ -237,6 +237,9 @@ Object.assign(frappe.utils, {
case "phone":
regExp = /^([0-9\ \+\_\-\,\.\*\#\(\)]){1,20}$/;
break;
case "name":
regExp = /^[\w][\w'-]*([ \w][\w'-]+)*$/;
break;
case "number":
regExp = /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/;
break;
@ -745,7 +748,36 @@ Object.assign(frappe.utils, {
});
return $el;
}
},
get_browser() {
var ua = navigator.userAgent,
tem,
M =
ua.match(
/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i
) || [];
if (/trident/i.test(M[1])) {
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
return { name: "IE", version: tem[1] || "" };
}
if (M[1] === "Chrome") {
tem = ua.match(/\bOPR|Edge\/(\d+)/);
if (tem != null) {
return { name: "Opera", version: tem[1] };
}
}
M = M[2]
? [M[1], M[2]]
: [navigator.appName, navigator.appVersion, "-?"];
if ((tem = ua.match(/version\/(\d+)/i)) != null) {
M.splice(1, 1, tem[1]);
}
return {
name: M[0],
version: M[1],
};
},
});
// Array de duplicate

View file

@ -6,9 +6,10 @@ frappe.breadcrumbs = {
preferred: {
"File": "",
"Video": "",
"Dashboard": "Customization",
"Dashboard Chart": "Customization",
"Dashboard Chart Source": "Customization",
"Dashboard Chart Source": "Customization"
},
module_map: {

View file

@ -20,7 +20,8 @@ frappe.views.CommunicationComposer = Class.extend({
primary_action: function() {
me.delete_saved_draft();
me.send_action();
}
},
minimizable: true
});
['recipients', 'cc', 'bcc'].forEach(field => {

View file

@ -344,10 +344,6 @@ class DesktopPage {
{
color: "orange",
description: __("No Records Created")
},
{
color: "red",
description: __("Has Open Entries")
}
].map(item => {
return `<div class="legend-item small text-muted justify-flex-start">

View file

@ -183,7 +183,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}
create_dashboard_chart(chart_args, dashboard_name, chart_name) {
let args = {
'dashboard': dashboard_name || null,
'chart_type': 'Report',
@ -191,8 +190,15 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
'type': chart_args.chart_type || frappe.model.unscrub(chart_args.type),
'color': chart_args.color,
'filters_json': JSON.stringify(this.get_filter_values()),
'custom_options': {}
};
for (let key in chart_args) {
if (key != "data") {
args['custom_options'][key] = chart_args[key];
}
}
if (this.chart_fields) {
let x_field_title = toTitle(chart_args.x_field);
let y_field_title = toTitle(chart_args.y_fields[0]);
@ -1084,7 +1090,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
], ({ file_format, include_indentation }) => {
this.make_access_log('Export', file_format);
if (file_format === 'CSV') {
const column_row = this.columns.map(col => col.label);
const column_row = this.columns.reduce((acc, col) => {
if (!col.hidden) {
acc.push(col.label);
}
return acc;
}, []);
const data = this.get_data_for_csv(include_indentation);
const out = [column_row].concat(data);

View file

@ -457,7 +457,8 @@ export default class ChartWidget extends Widget {
Line: "line",
Bar: "bar",
Percentage: "percentage",
Pie: "pie"
Pie: "pie",
Donut: "donut"
};
let colors = [];
@ -490,6 +491,14 @@ export default class ChartWidget extends Widget {
shortenYAxisNumbers: 1
}
};
if (this.chart_doc.custom_options) {
let custom_options = JSON.parse(this.chart_doc.custom_options);
for (let key in custom_options) {
chart_args[key] = custom_options[key];
}
}
if (!this.dashboard_chart) {
this.dashboard_chart = new frappe.Chart(
this.chart_wrapper[0],

View file

@ -770,6 +770,7 @@ h6.uppercase, .h6.uppercase {
.help-box {
margin-top: 3px;
margin-bottom: 6px;
}
pre {

View file

@ -0,0 +1,147 @@
@import "variables.less";
.module-head {
padding: 15px 30px;
border-bottom: 1px solid @light-border-color;
}
.module-head h1 {
padding: 0px;
margin: 0px;
}
.module-body {
padding: 0px 15px;
.section-head {
margin-bottom: 15px;
margin-top: 0px;
}
}
.module-section {
border-bottom: 1px solid @light-border-color;
.module-section-link {
line-height: 1.5em;
// font-size: 14px;
}
}
.module-section-column {
padding: 30px;
}
@media(min-width: @screen-xs) {
.module-section:nth-child(even) {
background-color: @light-bg;
}
.module-section:last-child {
border-bottom: none;
}
}
@media(max-width: @screen-sm) {
.module-body {
margin-top: 15px;
border-top: 1px solid @border-color;
}
}
@media(max-width: @screen-xs) {
.module-body {
margin-top: 0;
border-top: 1px solid transparent;
}
}
@media(max-width: @screen-xs) {
.module-section {
border: none;
}
.module-section-column {
border-bottom: 1px solid @light-border-color;
}
.module-section-column:nth-child(even) {
background-color: @light-bg;
}
.module-section:last-child .module-section-column:last-child {
border-bottom: none;
}
}
.module-item {
margin: 0px;
padding: 7px;
font-weight: 400;
border-bottom: 1px solid @border-color;
cursor: pointer;
transition: 0.2s;
-webkit-transition: 0.2s;
}
.module-item h4 {
display: inline-block;
}
.module-item .module-item-description {
margin-top: -5px;
}
.module-item .badge {
margin-top: -2px;
margin-left: 3px;
}
.module-item:hover, .module-item:focus {
background-color: @panel-bg;
}
.module-item:last-child {
border: none;
}
.module-link.active .icon-chevron-right {
margin-top: 4px;
display: block !important;
}
.module-item-progress {
margin-bottom: 10px;
height: 17px;
}
.module-item-progress-total {
height: 7px;
background-color: #999999;
width: 0px;
}
.module-item-progress-open {
height: 7px;
background-color: red;
width: 0px;
}
@media(max-width: @screen-xs) {
body[data-route^="Module"] {
.page-title {
width: 100%;
}
.page-actions {
display: none !important;
}
.layout-main-section {
border-bottom: 0px;
}
}
}

View file

@ -273,7 +273,8 @@ body[data-route^="Module"] .main-menu {
}
.layout-side-section .form-sidebar {
.modified-by {
.modified-by,
.pageview-count {
margin-bottom: 15px;
}
}

View file

@ -23,6 +23,17 @@ footer {
flex-shrink: 0;
}
// make navbar padding consistent with the page
.navbar {
padding-left: 0;
padding-right: 0;
.container {
padding-left: 15px;
padding-right: 15px;
}
}
.navbar.bg-dark {
.dropdown-menu {
font-size: .75rem;

View file

@ -64,6 +64,24 @@
{% if not only_static %}
{% block navbar_right_extension %}{% endblock %}
{% endif %}
{% if show_sidebar and sidebar_items %}
<div class="d-block d-sm-none">
<hr>
{% for item in sidebar_items -%}
<li class="nav-item">
{% if item.type != 'input' %}
<a href="{{ item.route }}" class="nav-link {{ 'text-dark' if pathname==item.route else 'text-muted'}}"
{% if item.target %}target="{{ item.target }}"{% endif %}>
{{ _(item.title or item.label) }}
</a>
{% endif %}
</li>
{%- endfor %}
<hr>
</div>
{% endif %}
{% include "templates/includes/navbar/navbar_search.html" %}
{% include "templates/includes/navbar/navbar_login.html" %}
</ul>

View file

@ -31,7 +31,6 @@
}
.ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;

View file

@ -13,7 +13,7 @@
</div>
{% block page_container %}
<main class="{% if not theme.use_full_width %}container{% endif %} my-5">
<main class="{% if not full_width %}container my-5{% endif %}">
<div class="d-flex justify-content-between align-items-center">
<div class="page-header">
{% block header %}{% endblock %}
@ -38,9 +38,11 @@
</div>
{% endmacro %}
{% macro container_attributes() %}
id="page-{{ name or route | e }}" data-path="{{ pathname | e }}" {%- if page_or_generator=="Generator" %}source-type="Generator" data-doctype="{{ doctype }}"{% endif %}
{% endmacro %}
{% macro container_attributes() -%}
id="page-{{ name or route | e }}" data-path="{{ pathname | e }}"
{%- if page_or_generator=="Generator" %}source-type="Generator" data-doctype="{{ doctype }}"{%- endif %}
{%- if source_content_type %}source-content-type="{{ source_content_type }}"{%- endif %}
{%- endmacro %}
{% if show_sidebar %}
<div class="container">

View file

@ -374,11 +374,11 @@ def delete_qrimage(user, check_expiry=False):
def delete_all_barcodes_for_users():
'''Task to delete all barcodes for user.'''
if not two_factor_is_enabled():
return
users = frappe.get_all('User', {'enabled':1})
for user in users:
if not two_factor_is_enabled(user=user.name):
continue
delete_qrimage(user.name, check_expiry=True)
def should_remove_barcode_image(barcode):

View file

@ -81,13 +81,29 @@ def validate_phone_number(phone_number, throw=False):
return False
phone_number = phone_number.strip()
match = re.match("([0-9\ \+\_\-\,\.\*\#\(\)]){1,20}$", phone_number)
match = re.match(r"([0-9\ \+\_\-\,\.\*\#\(\)]){1,20}$", phone_number)
if not match and throw:
frappe.throw(frappe._("{0} is not a valid Phone Number").format(phone_number), frappe.InvalidPhoneNumberError)
return bool(match)
def validate_name(name, throw=False):
"""Returns True if the name is valid
valid names may have unicode and ascii characters, dash, quotes, numbers
anything else is considered invalid
"""
if not name:
return False
name = name.strip()
match = re.match(r"^[\w][\w\'\-]*([ \w][\w\'\-]+)*$", name)
if not match and throw:
frappe.throw(frappe._("{0} is not a valid Name").format(name), frappe.InvalidNameError)
return bool(match)
def validate_email_address(email_str, throw=False):
"""Validates the email string"""
email = email_str = (email_str or "").strip()

View file

@ -174,9 +174,12 @@ def parse_latest_non_beta_release(response):
Returns
json : json object pertaining to the latest non-beta release
"""
for release in response:
if release['prerelease'] == True: continue
return release
version_list = [release.get('tag_name').strip('v') for release in response if not release.get('prerelease')]
if version_list:
return sorted(version_list, key=Version, reverse=True)[0]
return None
def check_release_on_github(app):
# Check if repo remote is on github
@ -199,12 +202,11 @@ def check_release_on_github(app):
org_name = remote_url.split('/')[3]
r = requests.get('https://api.github.com/repos/{}/{}/releases'.format(org_name, app))
if r.status_code == 200 and r.json():
if r.ok:
lastest_non_beta_release = parse_latest_non_beta_release(r.json())
return Version(lastest_non_beta_release['tag_name'].strip('v')), org_name
else:
# In case of an improper response or if there are no releases
return None
return Version(lastest_non_beta_release), org_name
# In case of an improper response or if there are no releases
return None
def add_message_to_redis(update_json):
# "update-message" will store the update message string

View file

@ -5,7 +5,7 @@ from logging.handlers import RotatingFileHandler
from six import text_type
default_log_level = logging.DEBUG
LOG_FILENAME = '../logs/frappe.log'
LOG_FILENAME = '../logs/{}-frappe.log'.format(frappe.local.site)
def get_logger(module, with_more_info=True):
if module in frappe.loggers:
@ -57,4 +57,3 @@ def set_log_level(level):
'''Use this method to set log level to something other than the default DEBUG'''
frappe.log_level = getattr(logging, (level or '').upper(), None) or default_log_level
frappe.loggers = {}

View file

@ -218,6 +218,6 @@ def send_private_file(path):
def handle_session_stopped():
frappe.respond_as_web_page(_("Updating"),
_("Your system is being updated. Please refresh again after a few moments"),
_("Your system is being updated. Please refresh again after a few moments."),
http_status_code=503, indicator_color='orange', fullpage = True, primary_action=None)
return frappe.website.render.render("message", http_status_code=503)

View file

@ -221,24 +221,24 @@ def add_metatags(context):
tags = frappe._dict(context.get("metatags") or {})
if tags:
if not "twitter:card" in tags:
tags["twitter:card"] = "summary_large_image"
if not "og:type" in tags:
tags["og:type"] = "article"
if tags.get("name"):
tags["og:title"] = tags["twitter:title"] = tags["name"]
name = tags.get('name') or tags.get('title')
if name:
tags["og:title"] = tags["twitter:title"] = name
if tags.get("title"):
tags["og:title"] = tags["twitter:title"] = tags["title"]
if tags.get("description"):
tags["og:description"] = tags["twitter:description"] = tags["description"]
description = tags.get("description") or context.description
if description:
tags['description'] = tags["og:description"] = tags["twitter:description"] = description
image = tags.get('image', context.image or None)
if image:
tags["og:image"] = tags["twitter:image:src"] = tags["image"] = frappe.utils.get_url(image)
tags['twitter:card'] = "summary_large_image"
if context.author or tags.get('author'):
tags['author'] = context.author or tags.get('author')
if context.path:
tags['og:url'] = tags['url'] = frappe.utils.get_url(context.path)
@ -246,11 +246,6 @@ def add_metatags(context):
if context.published_on:
tags['datePublished'] = context.published_on
if context.author:
tags['author'] = context.author
if context.description:
tags['description'] = context.description
tags['language'] = frappe.local.lang or 'en'

View file

@ -1,274 +1,108 @@
{
"allow_copy": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "field:short_name",
"beta": 0,
"creation": "2013-03-25 16:00:51",
"custom": 0,
"description": "User ID of a Blogger",
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"actions": [],
"allow_import": 1,
"autoname": "field:short_name",
"creation": "2013-03-25 16:00:51",
"description": "User ID of a Blogger",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"disabled",
"short_name",
"full_name",
"user",
"bio",
"avatar",
"posts"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "disabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Disabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Will be used in url (usually first name).",
"fieldname": "short_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Short Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"description": "Will be used in url (usually first name).",
"fieldname": "short_name",
"fieldtype": "Data",
"label": "Short Name",
"reqd": 1,
"unique": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "full_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Full Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "full_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Full Name",
"reqd": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "User",
"length": 0,
"no_copy": 0,
"options": "User",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "user",
"fieldtype": "Link",
"label": "User",
"options": "User"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "bio",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Bio",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "bio",
"fieldtype": "Small Text",
"label": "Bio"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "avatar",
"fieldtype": "Attach",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Avatar",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "avatar",
"fieldtype": "Attach",
"label": "Avatar"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "posts",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Posts",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "posts",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Posts",
"no_copy": 1,
"read_only": 1
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-user",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 1,
"modified": "2018-10-10 14:40:40.407657",
"modified_by": "Administrator",
"module": "Website",
"name": "Blogger",
"owner": "Administrator",
],
"icon": "fa fa-user",
"idx": 1,
"links": [
{
"link_doctype": "Blog Post",
"link_fieldname": "blogger"
}
],
"max_attachments": 1,
"modified": "2020-04-19 08:21:09.684300",
"modified_by": "Administrator",
"module": "Website",
"name": "Blogger",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 1,
"is_custom": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Website Manager",
"set_user_permissions": 1,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"import": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Website Manager",
"set_user_permissions": 1,
"share": 1,
"write": 1
},
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "Blogger",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"email": 1,
"print": 1,
"read": 1,
"role": "Blogger",
"share": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"title_field": "full_name",
"track_changes": 1,
"track_seen": 0
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "full_name",
"track_changes": 1
}

View file

View file

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

View file

@ -0,0 +1,44 @@
{
"actions": [],
"autoname": "Prompt",
"creation": "2020-04-19 02:25:37.010180",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"color"
],
"fields": [
{
"fieldname": "color",
"fieldtype": "Color",
"in_list_view": 1,
"label": "Color",
"reqd": 1
}
],
"links": [],
"modified": "2020-04-19 02:25:47.417772",
"modified_by": "Administrator",
"module": "Website",
"name": "Color",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Website Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class Color(Document):
pass

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestColor(unittest.TestCase):
pass

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_guest_to_view": 1,
"allow_import": 1,
"creation": "2013-03-28 10:35:30",
@ -13,6 +14,7 @@
"slideshow",
"cb1",
"published",
"full_width",
"show_title",
"start_date",
"end_date",
@ -39,6 +41,10 @@
"sb2",
"header",
"breadcrumbs",
"metatags_section",
"meta_title",
"meta_description",
"meta_image",
"set_meta_tags"
],
"fields": [
@ -217,7 +223,7 @@
"depends_on": "eval:!doc.__islocal",
"fieldname": "sb2",
"fieldtype": "Section Break",
"label": "Header, Breadcrumbs and Meta Tags"
"label": "Header and Breadcrumbs"
},
{
"description": "HTML for header section. Optional",
@ -235,21 +241,49 @@
{
"fieldname": "set_meta_tags",
"fieldtype": "Button",
"label": "Set Meta Tags"
"label": "Add Custom Tags"
},
{
"default": "0",
"fieldname": "dynamic_template",
"fieldtype": "Check",
"label": "Dynamic Template"
},
{
"default": "0",
"fieldname": "full_width",
"fieldtype": "Check",
"label": "Full Width"
},
{
"collapsible": 1,
"fieldname": "metatags_section",
"fieldtype": "Section Break",
"label": "Meta Tags"
},
{
"fieldname": "meta_title",
"fieldtype": "Data",
"label": "Title"
},
{
"fieldname": "meta_description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"fieldname": "meta_image",
"fieldtype": "Attach Image",
"label": "Image"
}
],
"has_web_view": 1,
"icon": "fa fa-file-alt",
"idx": 1,
"is_published_field": "published",
"links": [],
"max_attachments": 20,
"modified": "2019-10-02 13:58:50.825481",
"modified": "2020-04-19 12:26:21.546908",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Page",

View file

@ -36,6 +36,7 @@ class WebPage(WebsiteGenerator):
def get_context(self, context):
context.main_section = get_html_content_based_on_type(self, 'main_section', self.content_type)
context.source_content_type = self.content_type
self.render_dynamic(context)
# if static page, get static content
@ -127,13 +128,11 @@ class WebPage(WebsiteGenerator):
def set_metatags(self, context):
context.metatags = {
"name": context.title
"name": self.meta_title or self.title,
"description": self.meta_description,
"image": self.meta_image or find_first_image(context.main_section or "")
}
image = find_first_image(context.main_section or "")
if image:
context.metatags["image"] = image
def validate_dates(self):
if self.end_date:
if self.start_date and get_datetime(self.end_date) < get_datetime(self.start_date):

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestWebPageView(unittest.TestCase):
pass

View file

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

View file

@ -0,0 +1,75 @@
{
"actions": [],
"creation": "2020-04-15 22:54:46.009703",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"path",
"referrer",
"browser",
"browser_version",
"date"
],
"fields": [
{
"fieldname": "path",
"fieldtype": "Data",
"label": "Path",
"set_only_once": 1
},
{
"fieldname": "referrer",
"fieldtype": "Data",
"label": "Referrer",
"search_index": 1,
"set_only_once": 1
},
{
"fieldname": "browser",
"fieldtype": "Data",
"label": "Browser",
"search_index": 1,
"set_only_once": 1
},
{
"fieldname": "browser_version",
"fieldtype": "Data",
"label": "Browser Version",
"set_only_once": 1
},
{
"fieldname": "date",
"fieldtype": "Datetime",
"label": "Date",
"set_only_once": 1
}
],
"in_create": 1,
"links": [],
"modified": "2020-04-15 23:31:27.517793",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Page View",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "path",
"track_changes": 1
}

View file

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class WebPageView(Document):
pass
@frappe.whitelist(allow_guest=True)
def make_view_log(path, referrer=None, browser=None, version=None, url=None, user_tz=None):
request_dict = frappe.request.__dict__
user_agent = request_dict.get('environ', {}).get('HTTP_USER_AGENT')
is_unique = True
if referrer.startswith(url):
is_unique = False
if path.startswith('/'):
path = path[1:]
if is_tracking_enabled():
view = frappe.new_doc("Web Page View")
view.path = path
view.referrer = referrer
view.browser = browser
view.browser_version = version
view.time_zone = user_tz
view.user_agent = user_agent
view.is_unique = is_unique
view.insert(ignore_permissions=True)
return
@frappe.whitelist()
def get_page_view_count(path):
return frappe.db.count("Web Page View", filters={'path': path})
def is_tracking_enabled():
return frappe.db.get_value("Website Settings", "Website Settings", "enable_view_tracking")

View file

@ -9,7 +9,7 @@
{%- endif -%}
{%- macro render_element(element) -%}
{%- if element.element_type=='Content' -%}
{%- if element.element_type in ('Content', 'Web View') -%}
<div class="web-content {{ element_class(element) }}" {{ element_style(element) }}>
{{ element.web_content_html }}
</div>
@ -25,17 +25,16 @@
{%- endmacro -%}
{%- macro element_style(element) -%}
{%- if element.element_style -%}
style = "{{ element.element_style }}"
{%- if element.element_style or element.background_color -%}
style = "{{ element.element_style or '' }} {%if element.background_color %}background-color: {{ element.background_color }};{% endif %}"
{%- endif -%}
{%- endmacro -%}
{%- macro render_sections(sections) -%}
{%- for section in sections -%}
<section class='section {{ section.element_class or "" }} {{ section.hide and "hidden" or "" }}'>
<div class='section-body container'>
<section class='section {{ section.element_class or "" }} {{ section.hide and "hidden" or "" }}' {{ element_style(section) }}>
<div class='section-body {% if section.contain_section_width %}container{% endif %}'>
{%- if section.section_intro -%}
<div class='section-intro'>{{ section.section_intro }}</div>
{%- endif -%}
@ -74,4 +73,11 @@
{%- endif -%}
</div>
</section>
{%- endfor -%}
{%- endfor -%}
{%- endmacro -%}
{% if content_type == 'HTML' -%}
{{ content_html }}
{%- else -%}
{{ render_sections(sections) }}
{%- endif -%}

View file

@ -14,6 +14,7 @@ class TestWebView(unittest.TestCase):
@classmethod
def setUpClass(cls):
frappe.delete_doc_if_exists('Web View', 'test-web-view')
frappe.delete_doc_if_exists('Web View', 'html-web-view')
frappe.delete_doc_if_exists('CSS Class', 'test-css-class')
frappe.get_doc(dict(
@ -22,12 +23,25 @@ class TestWebView(unittest.TestCase):
css = '.test-class { color: red; }'
)).insert()
# simple html webview
frappe.get_doc(dict(
doctype = 'Web View',
title = 'HTML Web View',
route = 'html-web-view',
published = 1,
content_type = 'HTML',
content_html = '<h1>Hello HTML</h1>'
)).insert()
# simple web view with components
frappe.get_doc(dict(
doctype = 'Web View',
title = 'Test Web View',
route = 'test-web-view',
published = 1,
items = [
content_type = 'Components',
components = [
dict(
element_type = 'Section',
section_type = 'List'
@ -57,19 +71,27 @@ class TestWebView(unittest.TestCase):
web_content_type = 'Markdown',
web_content_markdown = 'Column 2'
),
dict(
element_type = 'Web View',
web_view = 'html-web-view',
),
]
)).insert()
def test_web_view(self):
html = get_page_content('test-web-view')
#print(html)
self.assert_web_view_in_html(html)
def test_html_web_view(self):
html = get_page_content('html-web-view')
self.assertTrue('Hello HTML' in html)
def assert_web_view_in_html(self, html):
self.assertTrue('<h2 id="heading">Heading</h2>' in html)
self.assertTrue('<div>Here is some HTML</div>' in html)
self.assertTrue('Column 1' in html)
self.assertTrue('Column 2' in html)
self.assertTrue('Hello HTML' in html)
self.assertTrue('.test-class { color: red; }' in html)
def test_web_view_in_footer(self):

View file

@ -3,7 +3,6 @@
"allow_guest_to_view": 1,
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:title",
"beta": 1,
"creation": "2020-03-16 15:28:03.828741",
"doctype": "DocType",
@ -12,18 +11,21 @@
"field_order": [
"title",
"route",
"column_break_4",
"full_width",
"published",
"items",
"css"
"section_break_6",
"content_type",
"content_html",
"components",
"style_section",
"css",
"metatags_section",
"meta_title",
"meta_description",
"meta_image"
],
"fields": [
{
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"options": "Web View Item",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
@ -36,8 +38,7 @@
"fieldname": "route",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Route",
"reqd": 1
"label": "Route"
},
{
"default": "0",
@ -49,12 +50,73 @@
"fieldname": "css",
"fieldtype": "Code",
"label": "CSS"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "full_width",
"fieldtype": "Check",
"label": "Full Width"
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"label": "Content"
},
{
"default": "Components",
"fieldname": "content_type",
"fieldtype": "Select",
"label": "Content Type",
"options": "Components\nHTML",
"reqd": 1
},
{
"depends_on": "eval:doc.content_type==='Components'",
"fieldname": "components",
"fieldtype": "Table",
"label": "Components",
"options": "Web View Component"
},
{
"depends_on": "eval:doc.content_type===\"HTML\"",
"fieldname": "content_html",
"fieldtype": "HTML Editor",
"label": "Content HTML"
},
{
"fieldname": "style_section",
"fieldtype": "Section Break",
"label": "Style"
},
{
"fieldname": "metatags_section",
"fieldtype": "Section Break",
"label": "Meta Tags"
},
{
"fieldname": "meta_title",
"fieldtype": "Data",
"label": "Title"
},
{
"fieldname": "meta_description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"fieldname": "meta_image",
"fieldtype": "Attach Image",
"label": "Image"
}
],
"has_web_view": 1,
"is_published_field": "published",
"links": [],
"modified": "2020-04-15 23:58:12.208049",
"modified": "2020-04-22 00:54:23.413077",
"modified_by": "Administrator",
"module": "Website",
"name": "Web View",

View file

@ -10,49 +10,71 @@ import frappe
class WebView(WebsiteGenerator):
def get_context(self, context):
# group items into sections
# group components into sections
if self.content_type=='Components':
self.build_components(context)
self.set_metatags(context)
return context
def build_components(self, context):
context.sections = []
context.css_rules = []
for item in self.items:
if not context.sections and item.element_type!='Section':
for component in self.components:
if not context.sections and component.element_type!='Section':
self.add_default_section(context)
if item.element_type=='Section':
self.add_section(context, item)
if component.element_type=='Section':
self.add_section(context, component)
else:
self.add_item(context, item)
self.add_component(context, component)
self.add_css_class(context, item)
self.add_css_class(context, component)
self.add_color(component)
self.add_missing_semi(component)
return context
def add_section(self, context, item):
item.elements = []
context.sections.append(item)
def add_section(self, context, component):
component.elements = []
context.sections.append(component)
if item.section_intro:
item.section_intro = markdown(item.section_intro)
if component.section_intro:
component.section_intro = markdown(component.section_intro)
def add_item(self, context, item):
if item.hide:
def add_component(self, context, component):
if component.hide:
return
if item.web_content_type == 'Markdown':
item.web_content_html = markdown(item.web_content_markdown)
if component.element_type == 'Web View' and component.web_view:
component.web_content_html = frappe.get_doc('Web View', component.web_view).render_content()
if item.title:
item.element_id = frappe.scrub(item.title)
elif component.web_content_type == 'Markdown':
component.web_content_html = markdown(component.web_content_markdown)
context.sections[-1].elements.append(item)
if component.title:
component.element_id = frappe.scrub(component.title)
def add_css_class(self, context, item):
context.sections[-1].elements.append(component)
def add_css_class(self, context, component):
# add css class definitions selected by the user
if item.element_class and not item.hide:
css, is_dynamic = frappe.db.get_value('CSS Class', item.element_class, ['css', 'is_dynamic'])
if component.element_class and not component.hide:
css, is_dynamic = frappe.db.get_value('CSS Class', component.element_class, ['css', 'is_dynamic'])
if is_dynamic:
css = frappe.render_template(css, self.get_theme())
context.css_rules.append(css)
def add_color(self, component):
# convert to css color
if component.background_color and not component.hide:
component.background_color = frappe.db.get_value('Color',
component.background_color, 'color', cache=True)
def add_missing_semi(self, component):
if component.element_style and not component.element_style.strip().endswith(';'):
component.element_style = component.element_style.strip() + ';'
def render_content(self):
# webview can be rendered as an object (see footer)
return frappe.render_template("frappe/website/doctype/web_view/templates/web_view_content.html", self.get_context(self.as_dict()))
@ -72,3 +94,11 @@ class WebView(WebsiteGenerator):
title='Default Section',
elements=[]
))
def set_metatags(self, context):
context.metatags = {
"name": self.meta_title or context.title,
"description": self.meta_description,
"image": self.meta_image
}

View file

@ -8,11 +8,14 @@
"element_type",
"title",
"hide",
"contain_section_width",
"column_break_3",
"columns",
"background_color",
"element_class",
"element_style",
"section_break_5",
"web_view",
"section_type",
"web_content_type",
"web_content_html",
@ -26,33 +29,35 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Element Type",
"options": "Section\nContent\nParagraph\nWeb List\nWeb Form",
"options": "Section\nContent\nImage\nWeb View",
"reqd": 1
},
{
"default": "List",
"depends_on": "eval:doc.element_type==='Section'",
"fieldname": "section_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Section Type",
"options": "\nList\nTabbed\nGrid"
"options": "List\nTabbed\nGrid"
},
{
"default": "Markdown",
"depends_on": "eval:doc.element_type==='Content'",
"fieldname": "web_content_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Web Content Type",
"options": "\nHTML\nMarkdown"
"options": "Markdown\nHTML"
},
{
"depends_on": "eval:doc.web_content_type==='HTML'",
"depends_on": "eval:doc.element_type === 'Content' && doc.web_content_type === 'HTML'",
"fieldname": "web_content_html",
"fieldtype": "HTML Editor",
"label": "Web Content HTML"
},
{
"depends_on": "eval:doc.web_content_type==='Markdown'",
"depends_on": "eval:doc.element_type === 'Content' && doc.web_content_type === 'Markdown'",
"fieldname": "web_content_markdown",
"fieldtype": "Markdown Editor",
"label": "Web Content Markdown"
@ -104,14 +109,34 @@
"fieldname": "element_style",
"fieldtype": "Small Text",
"label": "Element Style"
},
{
"default": "0",
"depends_on": "eval:doc.element_type==='Section'",
"fieldname": "contain_section_width",
"fieldtype": "Check",
"label": "Contain Section Width"
},
{
"fieldname": "background_color",
"fieldtype": "Link",
"label": "Background Color",
"options": "Color"
},
{
"depends_on": "eval:doc.element_type==='Web View'",
"fieldname": "web_view",
"fieldtype": "Link",
"label": "Web View",
"options": "Web View"
}
],
"istable": 1,
"links": [],
"modified": "2020-03-28 14:21:50.014823",
"modified": "2020-04-19 03:02:53.233036",
"modified_by": "Administrator",
"module": "Website",
"name": "Web View Item",
"name": "Web View Component",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class WebViewComponent(Document):
pass

View file

@ -56,6 +56,10 @@ frappe.ui.form.on('Website Settings', {
});
},
enable_view_tracking: function(frm) {
frappe.boot.website_tracking_enabled = frm.doc.enable_view_tracking;
},
set_parent_options: function(frm, doctype, name) {
var item = frappe.get_doc(doctype, name);
if(item.parentfield === "top_bar_items") {

View file

@ -33,6 +33,7 @@
"footer_items",
"hide_footer_signup",
"integrations",
"enable_view_tracking",
"enable_google_indexing",
"authorize_api_indexing_access",
"indexing_refresh_token",
@ -196,7 +197,7 @@
"collapsible": 1,
"fieldname": "integrations",
"fieldtype": "Section Break",
"label": "Google Integrations"
"label": "Integrations"
},
{
"description": "Add Google Analytics ID: eg. UA-89XXX57-1. Please search help on Google Analytics for more information.",
@ -330,6 +331,12 @@
"fieldtype": "Button",
"label": "Authorize API Indexing Access"
},
{
"default": "0",
"fieldname": "enable_view_tracking",
"fieldtype": "Check",
"label": "Enable In App Website Tracking"
},
{
"default": "Standard",
"fieldname": "footer_type",
@ -364,7 +371,7 @@
"issingle": 1,
"links": [],
"max_attachments": 10,
"modified": "2020-04-21 16:46:59.947403",
"modified": "2020-04-21 12:37:44.070662",
"modified_by": "Administrator",
"module": "Website",
"name": "Website Settings",

View file

@ -118,7 +118,7 @@ def get_website_settings():
for k in ["banner_html", "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"]:
"navbar_search", "enable_view_tracking"]:
if hasattr(settings, k):
context[k] = settings.get(k)

View file

@ -14,8 +14,10 @@
"google_font",
"font_size",
"font_properties",
"use_full_width",
"column_break_7",
"button_rounded_corners",
"button_shadows",
"button_gradients",
"column_break_11",
"primary_color",
"text_color",
"light_color",
@ -99,29 +101,29 @@
"fieldtype": "Data",
"label": "Font Size"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"fieldname": "primary_color",
"fieldtype": "Color",
"label": "Primary Color"
"fieldtype": "Link",
"label": "Primary Color",
"options": "Color"
},
{
"fieldname": "text_color",
"fieldtype": "Color",
"label": "Text Color"
"fieldtype": "Link",
"label": "Text Color",
"options": "Color"
},
{
"fieldname": "dark_color",
"fieldtype": "Color",
"label": "Dark Color"
"fieldtype": "Link",
"label": "Dark Color",
"options": "Color"
},
{
"fieldname": "background_color",
"fieldtype": "Color",
"label": "Background Color"
"fieldtype": "Link",
"label": "Background Color",
"options": "Color"
},
{
"fieldname": "stylesheet_section",
@ -135,8 +137,9 @@
},
{
"fieldname": "light_color",
"fieldtype": "Color",
"label": "Light Color"
"fieldtype": "Link",
"label": "Light Color",
"options": "Color"
},
{
"default": "300,600",
@ -145,14 +148,30 @@
"label": "Font Properties"
},
{
"description": "Content will not be inside a \"container\" class, you will have to add your own containers for different sections.",
"fieldname": "use_full_width",
"fieldtype": "Data",
"label": "Use Full Width"
"default": "1",
"fieldname": "button_rounded_corners",
"fieldtype": "Check",
"label": "Button Rounded Corners"
},
{
"default": "0",
"fieldname": "button_shadows",
"fieldtype": "Check",
"label": "Button Shadows"
},
{
"default": "0",
"fieldname": "button_gradients",
"fieldtype": "Check",
"label": "Button Gradients"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
}
],
"links": [],
"modified": "2020-03-19 09:46:48.750150",
"modified": "2020-04-19 05:18:49.820803",
"modified_by": "Administrator",
"module": "Website",
"name": "Website Theme",

View file

@ -25,7 +25,7 @@ class WebsiteTheme(Document):
def is_standard_and_not_valid_user(self):
return (not self.custom
and not frappe.local.conf.get('developer_mode')
and not (frappe.flags.in_import or frappe.flags.in_test))
and not (frappe.flags.in_import or frappe.flags.in_test or frappe.flags.in_migrate))
def on_trash(self):
if self.is_standard_and_not_valid_user():
@ -61,10 +61,13 @@ class WebsiteTheme(Document):
from subprocess import Popen, PIPE
folder_path = join_path(frappe.utils.get_bench_path(), 'sites', 'assets', 'css')
self.delete_old_theme_files(folder_path)
if not self.custom:
self.delete_old_theme_files(folder_path)
# add a random suffix
file_name = frappe.scrub(self.name) + '_' + frappe.generate_hash('Website Theme', 8) + '.css'
suffix = frappe.generate_hash('Website Theme', 8) if self.custom else 'style'
file_name = frappe.scrub(self.name) + '_' + suffix + '.css'
output_path = join_path(folder_path, file_name)
content = get_scss(self)

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