Merge branch 'develop' into custom_dashbaiord_options

This commit is contained in:
Suraj Shetty 2020-04-25 17:38:18 +05:30 committed by GitHub
commit e2bf18f1b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
102 changed files with 1607 additions and 979 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

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

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

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

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

@ -86,6 +86,14 @@ frappe.Application = Class.extend({
this.show_update_available();
}
if (!frappe.boot.developer_mode) {
let console_security_message = __("Using this console may allow attackers to impersonate you and steal your information. Do not enter or paste code that you do not understand.");
console.log(
`%c${console_security_message}`,
"font-size: large"
);
}
this.show_notes();
if (frappe.boot.is_first_startup) {

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

@ -125,8 +125,9 @@ frappe.ui.form.Dashboard = Class.extend({
},
format_percent: function(title, percent) {
var width = cint(percent) < 1 ? 1 : cint(percent);
var progress_class = "progress-bar-success";
const percentage = cint(percent);
const width = percentage < 0 ? 100 : percentage;
const progress_class = percentage < 0 ? "progress-bar-danger" : "progress-bar-success";
return [{
title: title,

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

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

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

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

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

View file

@ -1,21 +1,23 @@
{% if google_font %}
@import url('https://fonts.googleapis.com/css?family={{ google_font.replace(' ', '+') }}:{{ font_properties }}&display=swap');
$font-family-sans-serif: "{{ google_font }}", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
{% endif %}
{% if primary_color %}$primary: {{ primary_color }};{% endif %}
{% if dark_color %}$dark: {{ dark_color }};{% endif %}
{% if text_color %}$body-color: {{ text_color }};{% endif %}
{% if background_color %}$body-bg: {{ background_color }};{% endif %}
{% endif -%}
$enable-shadows: {{ enable_shadows and "true" or "false" }};
$enable-gradients: {{ enable_gradients and "true" or "false" }};
$enable-rounded: {{ enable_rounded and "true" or "false" }};
{% if primary_color %}$primary: {{ frappe.db.get_value('Color', primary_color, 'color') }};{% endif -%}
{% if dark_color %}$dark: {{ frappe.db.get_value('Color', dark_color, 'color') }};{% endif -%}
{% if text_color %}$body-color: {{ frappe.db.get_value('Color', text_color, 'color') }};{% endif -%}
{% if background_color %}$body-bg: {{ frappe.db.get_value('Color', background_color, 'color') }};{% endif -%}
$enable-shadows: {{ button_shadows and "true" or "false" }};
$enable-gradients: {{ button_gradients and "true" or "false" }};
$enable-rounded: {{ button_rounded_corners and "true" or "false" }};
@import "frappe/public/scss/website";
{% if font_size -%}
body {
{% if font_size %}
font-size: {{ font_size }};
{% endif %}
}
{%- endif %}

View file

@ -1,26 +1,20 @@
{
"apply_style": 0,
"apply_text_styles": 0,
"creation": "2015-02-19 13:37:33.925909",
"css": ".navbar-header {\n display: none;\n}",
"custom": 0,
"docstatus": 0,
"doctype": "Website Theme",
"font_size": "14px",
"footer_color": "",
"footer_text_color": "",
"heading_style": "",
"heading_webfont": "",
"idx": 26,
"link_color": "",
"modified": "2016-12-29 05:40:17.289226",
"modified_by": "Administrator",
"module": "Website",
"name": "Standard",
"owner": "Administrator",
"text_color": "",
"text_webfont": "",
"theme": "Standard",
"top_bar_color": "",
"top_bar_text_color": ""
"button_gradients": 0,
"button_rounded_corners": 1,
"button_shadows": 0,
"creation": "2015-02-19 13:37:33.925909",
"custom": 0,
"docstatus": 0,
"doctype": "Website Theme",
"font_properties": "300,600",
"font_size": "",
"idx": 27,
"modified": "2020-04-21 02:10:31.761219",
"modified_by": "Administrator",
"module": "Website",
"name": "Standard",
"owner": "Administrator",
"theme": "Standard",
"theme_scss": "$enable-shadows: false;\n$enable-gradients: false;\n$enable-rounded: true;\n\n@import \"frappe/public/scss/website\";\n\n",
"theme_url": "/assets/css/standard_style.css"
}

View file

@ -9,7 +9,7 @@
<span class='indicator blue password-box'>{{ _("Reset Password") if frappe.db.get_default('company') else _("Set Password")}}</span>
</div>
<form id="reset-password">
<div class="form-group">
<div class="form-group" style="display: none;">
<input id="old_password" type="password"
class="form-control" placeholder="{{ _("Old Password") }}">
</div>
@ -32,8 +32,8 @@
<script>
frappe.ready(function() {
if(frappe.utils.get_url_arg("key")) {
$("#old_password").parent().toggle(false);
if(!frappe.utils.get_url_arg("key")) {
$("#old_password").parent().toggle();
}
if(frappe.utils.get_url_arg("password_expired")) {
@ -57,12 +57,10 @@ frappe.ready(function() {
}
if(!args.old_password && !args.key) {
frappe.msgprint("{{ _("Old Password Required.") }}");
return;
frappe.msgprint(__("Old Password Required."));
}
if(!args.new_password) {
frappe.msgprint("{{ _("New Password Required.") }}");
return;
frappe.msgprint(__("New Password Required."));
}
frappe.call({
type: "POST",
@ -71,19 +69,24 @@ frappe.ready(function() {
args: args,
statusCode: {
401: function() {
$('.page-card-head .indicator').removeClass().addClass('indicator red')
.text("{{ _('Invalid Password') }}");
$(".page-card-head .indicator").removeClass().addClass("indicator red").text(__("Invalid Password"));
},
410: function({ responseJSON }) {
const title = __("Invalid Link");
const message = responseJSON.message;
$(".page-card-head .indicator").removeClass().addClass("indicator grey").text(title);
frappe.msgprint({ title: title, message: message, clear: true });
},
200: function(r) {
$("input").val("");
strength_indicator.addClass('hidden');
strength_message.addClass('hidden');
$('.page-card-head .indicator')
.removeClass().addClass('indicator green')
.html("{{ _('Password Updated') }}");
strength_indicator.addClass("hidden");
strength_message.addClass("hidden");
$(".page-card-head .indicator")
.removeClass().addClass("indicator blue")
.html(__("Status Updated"));
if(r.message) {
frappe.msgprint({
message: "{{ _("Password Updated") }}",
message: __("Password Updated"),
// password is updated successfully
// clear any server message
clear: true

View file

@ -12,3 +12,19 @@ ga('create', '{{ google_analytics_id }}', 'auto');
ga('send', 'pageview');
// End Google Analytics
{%- endif %}
{% if enable_view_tracking %}
if (navigator.doNotTrack != 1) {
frappe.ready(() => {
let browser = frappe.utils.get_browser();
frappe.call("frappe.website.doctype.web_page_view.web_page_view.make_view_log", {
path: location.pathname,
referrer: document.referrer,
browser: browser.name,
version: browser.version,
url: location.origin,
user_tz: Intl.DateTimeFormat().resolvedOptions().timeZone
})
})
}
{% endif %}

View file

@ -4,7 +4,6 @@
"build": "node rollup/build.js",
"production": "FRAPPE_ENV=production node rollup/build.js",
"watch": "node rollup/watch.js",
"cypress:run": "cypress run --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb",
"cypress:open": "cypress open",
"snyk-protect": "snyk protect",
"prepare": "yarn run snyk-protect"
@ -41,17 +40,17 @@
"qz-tray": "^2.0.8",
"redis": "^2.8.0",
"showdown": "^1.9.1",
"snyk": "^1.297.4",
"socket.io": "^2.3.0",
"superagent": "^3.8.2",
"touch": "^3.1.0",
"vue": "^2.6.11",
"vue-router": "^2.0.0",
"snyk": "^1.297.4"
"vue-router": "^2.0.0"
},
"devDependencies": {
"babel-runtime": "^6.26.0",
"chalk": "^2.3.2",
"cypress": "^3.1.1",
"cypress": "3",
"cypress-file-upload": "^3.1.0",
"less": "^3.11.1",
"node-sass": "^4.13.1",

View file

@ -11,6 +11,7 @@ const buble = require('rollup-plugin-buble');
const { terser } = require('rollup-plugin-terser');
const vue = require('rollup-plugin-vue');
const frappe_html = require('./frappe-html-plugin');
const less_loader = require('./less-loader');
const production = process.env.FRAPPE_ENV === 'production';
@ -116,6 +117,7 @@ function get_rollup_options_for_css(output_file, input_files) {
// less -> css
postcss({
extract: output_path,
loaders: [less_loader],
use: [
['less', {
// import other less/css files starting from these folders
@ -130,7 +132,8 @@ function get_rollup_options_for_css(output_file, input_files) {
path.resolve(bench_path, '**/*.scss'),
path.resolve(bench_path, '**/*.css')
],
minimize: minimize_css
minimize: minimize_css,
sourceMap: output_file.startsWith('css/') && !production
})
];

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