Merge branch 'develop' into discussions-component-redesign
This commit is contained in:
commit
653bb909d4
68 changed files with 1336 additions and 658 deletions
|
|
@ -7,8 +7,8 @@ context('Web Form', () => {
|
|||
cy.visit('/update-profile');
|
||||
cy.get_field('last_name', 'Data').type('_Test User', {force: true}).wait(200);
|
||||
cy.get('.web-form-actions .btn-primary').click();
|
||||
cy.wait(500);
|
||||
cy.get('.modal.show > .modal-dialog').should('be.visible');
|
||||
cy.wait(5000);
|
||||
cy.url().should('include', '/me');
|
||||
});
|
||||
|
||||
it('Navigate and Submit a MultiStep WebForm', () => {
|
||||
|
|
@ -16,14 +16,12 @@ context('Web Form', () => {
|
|||
cy.visit('/update-profile-duplicate');
|
||||
cy.get_field('last_name', 'Data').type('_Test User', {force: true}).wait(200);
|
||||
cy.get('.btn-next').should('be.visible');
|
||||
cy.get('.web-form-footer .btn-primary').should('not.be.visible');
|
||||
cy.get('.btn-next').click();
|
||||
cy.get('.btn-previous').should('be.visible');
|
||||
cy.get('.btn-next').should('not.be.visible');
|
||||
cy.get('.web-form-footer .btn-primary').should('be.visible');
|
||||
cy.get('.web-form-actions .btn-primary').click();
|
||||
cy.wait(500);
|
||||
cy.get('.modal.show > .modal-dialog').should('be.visible');
|
||||
cy.wait(5000);
|
||||
cy.url().should('include', '/me');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -728,7 +728,7 @@ def move(dest_dir, site):
|
|||
@click.command('set-password')
|
||||
@click.argument('user')
|
||||
@click.argument('password', required=False)
|
||||
@click.option('--logout-all-sessions', help='Logout from all sessions', is_flag=True, default=False)
|
||||
@click.option('--logout-all-sessions', help='Log out from all sessions', is_flag=True, default=False)
|
||||
@pass_context
|
||||
def set_password(context, user, password=None, logout_all_sessions=False):
|
||||
"Set password for a user on a site"
|
||||
|
|
@ -741,7 +741,7 @@ def set_password(context, user, password=None, logout_all_sessions=False):
|
|||
|
||||
@click.command('set-admin-password')
|
||||
@click.argument('admin-password', required=False)
|
||||
@click.option('--logout-all-sessions', help='Logout from all sessions', is_flag=True, default=False)
|
||||
@click.option('--logout-all-sessions', help='Log out from all sessions', is_flag=True, default=False)
|
||||
@pass_context
|
||||
def set_admin_password(context, admin_password=None, logout_all_sessions=False):
|
||||
"Set Administrator password for a site"
|
||||
|
|
|
|||
|
|
@ -2,20 +2,17 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
from datetime import datetime
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import now_datetime, add_to_date
|
||||
from frappe.core.doctype.log_settings.log_settings import run_log_clean_up
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestLogSettings(unittest.TestCase):
|
||||
class TestLogSettings(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.savepoint = "TestLogSettings"
|
||||
# SAVEPOINT can only be used in transaction blocks and we don't wan't to take chances
|
||||
frappe.db.begin()
|
||||
frappe.db.savepoint(cls.savepoint)
|
||||
super().setUpClass()
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Log Settings",
|
||||
|
|
@ -26,10 +23,6 @@ class TestLogSettings(unittest.TestCase):
|
|||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.db.rollback(save_point=cls.savepoint)
|
||||
|
||||
def setUp(self) -> None:
|
||||
if self._testMethodName == "test_delete_logs":
|
||||
self.datetime = frappe._dict()
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from frappe.modules import make_boilerplate
|
|||
from frappe.core.doctype.page.page import delete_custom_role
|
||||
from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles
|
||||
from frappe.desk.reportview import append_totals_row
|
||||
from frappe.utils.safe_exec import safe_exec
|
||||
from frappe.utils.safe_exec import safe_exec, check_safe_sql_query
|
||||
|
||||
|
||||
class Report(Document):
|
||||
|
|
@ -110,8 +110,7 @@ class Report(Document):
|
|||
if not self.query:
|
||||
frappe.throw(_("Must specify a Query to run"), title=_('Report Document Error'))
|
||||
|
||||
if not self.query.lower().startswith("select"):
|
||||
frappe.throw(_("Query must be a SELECT"), title=_('Report Document Error'))
|
||||
check_safe_sql_query(self.query)
|
||||
|
||||
result = [list(t) for t in frappe.db.sql(self.query, filters)]
|
||||
columns = self.get_columns() or [cstr(c[0]) for c in frappe.db.get_description()]
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import textwrap
|
||||
|
||||
import frappe, json, os
|
||||
import unittest
|
||||
from frappe.desk.query_report import run, save_report, add_total_row
|
||||
from frappe.desk.reportview import delete_report, save_report as _save_report
|
||||
from frappe.custom.doctype.customize_form.customize_form import reset_customization
|
||||
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
test_records = frappe.get_test_records('Report')
|
||||
test_dependencies = ['User']
|
||||
|
||||
class TestReport(unittest.TestCase):
|
||||
class TestReport(FrappeTestCase):
|
||||
def test_report_builder(self):
|
||||
if frappe.db.exists('Report', 'User Activity Report'):
|
||||
frappe.delete_doc('Report', 'User Activity Report')
|
||||
|
|
@ -335,3 +337,29 @@ result = [
|
|||
self.assertEqual(result[-1][0], "Total")
|
||||
self.assertEqual(result[-1][1], 200)
|
||||
self.assertEqual(result[-1][2], 150.50)
|
||||
|
||||
def test_cte_in_query_report(self):
|
||||
cte_query = textwrap.dedent("""
|
||||
with enabled_users as (
|
||||
select name
|
||||
from `tabUser`
|
||||
where enabled = 1
|
||||
)
|
||||
select * from enabled_users;
|
||||
""")
|
||||
|
||||
report = frappe.get_doc({
|
||||
"doctype": "Report",
|
||||
"ref_doctype": "User",
|
||||
"report_name": "Enabled Users List",
|
||||
"report_type": "Query Report",
|
||||
"is_standard": "No",
|
||||
"query": cte_query,
|
||||
}).insert()
|
||||
|
||||
if frappe.db.db_type == "mariadb":
|
||||
col, rows = report.execute_query_report(filters={})
|
||||
self.assertEqual(col[0], "name")
|
||||
self.assertGreaterEqual(len(rows), 1)
|
||||
elif frappe.db.db_type == "postgres":
|
||||
self.assertRaises(frappe.PermissionError, report.execute_query_report, filters={})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2014-03-11 14:55:00",
|
||||
"creation": "2022-01-10 17:29:51.672911",
|
||||
"description": "Represents a User in the system.",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
|
|
@ -48,6 +48,12 @@
|
|||
"document_follow_notifications_section",
|
||||
"document_follow_notify",
|
||||
"document_follow_frequency",
|
||||
"column_break_75",
|
||||
"follow_created_documents",
|
||||
"follow_commented_documents",
|
||||
"follow_liked_documents",
|
||||
"follow_assigned_documents",
|
||||
"follow_shared_documents",
|
||||
"email_settings",
|
||||
"email_signature",
|
||||
"thread_notify",
|
||||
|
|
@ -606,6 +612,45 @@
|
|||
"fieldtype": "Link",
|
||||
"label": "Module Profile",
|
||||
"options": "Module Profile"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_75",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.document_follow_notify== 1)",
|
||||
"fieldname": "follow_created_documents",
|
||||
"fieldtype": "Check",
|
||||
"label": "Auto follow documents that you create"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.document_follow_notify== 1)",
|
||||
"fieldname": "follow_commented_documents",
|
||||
"fieldtype": "Check",
|
||||
"label": "Auto follow documents that you comment on"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.document_follow_notify== 1)",
|
||||
"fieldname": "follow_liked_documents",
|
||||
"fieldtype": "Check",
|
||||
"label": "Auto follow documents that you Like"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.document_follow_notify== 1)",
|
||||
"fieldname": "follow_shared_documents",
|
||||
"fieldtype": "Check",
|
||||
"label": "Auto follow documents that are shared with you"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.document_follow_notify== 1)",
|
||||
"fieldname": "follow_assigned_documents",
|
||||
"fieldtype": "Check",
|
||||
"label": "Auto follow documents that are assigned to you"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-user",
|
||||
|
|
@ -704,4 +749,4 @@
|
|||
"states": [],
|
||||
"title_field": "full_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,133 +1,159 @@
|
|||
{
|
||||
"accept_payment": 0,
|
||||
"allow_comments": 0,
|
||||
"allow_delete": 0,
|
||||
"allow_edit": 1,
|
||||
"allow_incomplete": 0,
|
||||
"allow_multiple": 0,
|
||||
"allow_print": 0,
|
||||
"amount": 0.0,
|
||||
"amount_based_on_field": 0,
|
||||
"breadcrumbs": "[{\"title\": _(\"My Account\"), \"route\": \"me\"}]",
|
||||
"creation": "2016-09-19 05:16:59.242754",
|
||||
"doc_type": "User",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Form",
|
||||
"idx": 0,
|
||||
"introduction_text": "",
|
||||
"is_standard": 1,
|
||||
"login_required": 1,
|
||||
"max_attachment_size": 0,
|
||||
"modified": "2019-01-28 12:45:17.158069",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "edit-profile",
|
||||
"owner": "Administrator",
|
||||
"published": 1,
|
||||
"route": "update-profile",
|
||||
"show_in_grid": 0,
|
||||
"show_sidebar": 1,
|
||||
"sidebar_items": [],
|
||||
"success_message": "Profile updated successfully.",
|
||||
"success_url": "/me",
|
||||
"title": "Update Profile",
|
||||
"accept_payment": 0,
|
||||
"allow_comments": 0,
|
||||
"allow_delete": 0,
|
||||
"allow_edit": 1,
|
||||
"allow_incomplete": 0,
|
||||
"allow_multiple": 0,
|
||||
"allow_print": 0,
|
||||
"amount": 0.0,
|
||||
"amount_based_on_field": 0,
|
||||
"apply_document_permissions": 0,
|
||||
"breadcrumbs": "[{\"title\": _(\"My Account\"), \"route\": \"me\"}]",
|
||||
"creation": "2016-09-19 05:16:59.242754",
|
||||
"doc_type": "User",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Form",
|
||||
"idx": 0,
|
||||
"introduction_text": "",
|
||||
"is_multi_step_form": 0,
|
||||
"is_standard": 1,
|
||||
"login_required": 1,
|
||||
"max_attachment_size": 0,
|
||||
"modified": "2022-03-22 15:00:43.456738",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "edit-profile",
|
||||
"owner": "Administrator",
|
||||
"published": 1,
|
||||
"route": "update-profile",
|
||||
"route_to_success_link": 0,
|
||||
"show_attachments": 0,
|
||||
"show_in_grid": 0,
|
||||
"show_sidebar": 0,
|
||||
"sidebar_items": [],
|
||||
"success_message": "Profile updated successfully.",
|
||||
"success_url": "/me",
|
||||
"title": "Update Profile",
|
||||
"web_form_fields": [
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "first_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "First Name",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 1,
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "first_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "First Name",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 1,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "middle_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Middle Name (Optional)",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "middle_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Middle Name (Optional)",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "last_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Last Name",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "last_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Last Name",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"description": "",
|
||||
"fieldname": "user_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 0,
|
||||
"label": "User Image",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"label": "More Information",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"description": "",
|
||||
"fieldname": "user_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 0,
|
||||
"label": "Profile Picture",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "phone",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Phone",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"label": "More Information",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Mobile Number",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "phone",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Phone",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"description": "",
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"label": "Language",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"options": "Language",
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Mobile Number",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"description": "",
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"label": "Language",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"options": "Language",
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -84,7 +84,8 @@ def add(args=None):
|
|||
shared_with_users.append(assign_to)
|
||||
|
||||
# make this document followed by assigned user
|
||||
follow_document(args['doctype'], args['name'], assign_to)
|
||||
if frappe.get_cached_value("User", assign_to, "follow_assigned_documents"):
|
||||
follow_document(args['doctype'], args['name'], assign_to)
|
||||
|
||||
# notify
|
||||
notify_assignment(d.assigned_by, d.allocated_to, d.reference_type, d.reference_name, action='ASSIGN',
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import frappe.utils
|
|||
from frappe.utils import get_url_to_form
|
||||
from frappe.model import log_types
|
||||
from frappe import _
|
||||
from itertools import groupby
|
||||
from frappe.query_builder import DocType
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_follow(doctype, doc_name, following):
|
||||
|
|
@ -94,33 +94,50 @@ def send_document_follow_mails(frequency):
|
|||
call method to send mail
|
||||
'''
|
||||
|
||||
users = frappe.get_list("Document Follow",
|
||||
fields=["*"])
|
||||
user_list = get_user_list(frequency)
|
||||
|
||||
sorted_users = sorted(users, key=lambda k: k['user'])
|
||||
for user in user_list:
|
||||
message, valid_document_follows = get_message_for_user(frequency, user)
|
||||
if message:
|
||||
send_email_alert(user, valid_document_follows, message)
|
||||
# send an email if we have already spent resources creating the message
|
||||
# nosemgrep
|
||||
frappe.db.commit()
|
||||
|
||||
grouped_by_user = {}
|
||||
for k, v in groupby(sorted_users, key=lambda k: k['user']):
|
||||
grouped_by_user[k] = list(v)
|
||||
def get_user_list(frequency):
|
||||
DocumentFollow = DocType('Document Follow')
|
||||
User = DocType('User')
|
||||
return (frappe.qb.from_(DocumentFollow).join(User)
|
||||
.on(DocumentFollow.user == User.name)
|
||||
.where(User.document_follow_notify == 1)
|
||||
.where(User.document_follow_frequency == frequency)
|
||||
.select(DocumentFollow.user)
|
||||
.groupby(DocumentFollow.user)).run(pluck="user")
|
||||
|
||||
for user in grouped_by_user:
|
||||
user_frequency = frappe.db.get_value("User", user, "document_follow_frequency")
|
||||
message = []
|
||||
valid_document_follows = []
|
||||
if user_frequency == frequency:
|
||||
for d in grouped_by_user[user]:
|
||||
content = get_message(d.ref_docname, d.ref_doctype, frequency, user)
|
||||
if content:
|
||||
message = message + content
|
||||
valid_document_follows.append({
|
||||
"reference_docname": d.ref_docname,
|
||||
"reference_doctype": d.ref_doctype,
|
||||
"reference_url": get_url_to_form(d.ref_doctype, d.ref_docname)
|
||||
})
|
||||
def get_message_for_user(frequency, user):
|
||||
message = []
|
||||
latest_document_follows = get_document_followed_by_user(user)
|
||||
valid_document_follows = []
|
||||
|
||||
if message and frappe.db.get_value("User", user, "document_follow_notify", ignore=True):
|
||||
send_email_alert(user, valid_document_follows, message)
|
||||
for document_follow in latest_document_follows:
|
||||
content = get_message(document_follow.ref_docname, document_follow.ref_doctype, frequency, user)
|
||||
if content:
|
||||
message = message + content
|
||||
valid_document_follows.append({
|
||||
"reference_docname": document_follow.ref_docname,
|
||||
"reference_doctype": document_follow.ref_doctype,
|
||||
"reference_url": get_url_to_form(document_follow.ref_doctype, document_follow.ref_docname)
|
||||
})
|
||||
return message, valid_document_follows
|
||||
|
||||
def get_document_followed_by_user(user):
|
||||
DocumentFollow = DocType('Document Follow')
|
||||
# at max 20 documents are sent for each user
|
||||
return (frappe.qb.from_(DocumentFollow)
|
||||
.where(DocumentFollow.user == user)
|
||||
.select(DocumentFollow.ref_doctype, DocumentFollow.ref_docname)
|
||||
.orderby(DocumentFollow.modified)
|
||||
.limit(20)).run(as_dict=True)
|
||||
|
||||
def get_version(doctype, doc_name, frequency, user):
|
||||
timeline = []
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ def add_comment(reference_doctype, reference_name, content, comment_email, comme
|
|||
reference_doc = frappe.get_doc(reference_doctype, reference_name)
|
||||
doc.content = extract_images_from_html(reference_doc, content, is_private=True)
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
follow_document(doc.reference_doctype, doc.reference_name, frappe.session.user)
|
||||
if frappe.get_cached_value("User", frappe.session.user, "follow_commented_documents"):
|
||||
follow_document(doc.reference_doctype, doc.reference_name, frappe.session.user)
|
||||
return doc.as_dict()
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ def _toggle_like(doctype, name, add, user=None):
|
|||
if user not in liked_by:
|
||||
liked_by.append(user)
|
||||
add_comment(doctype, name)
|
||||
follow_document(doctype, name, user)
|
||||
if frappe.get_cached_value("User", user, "follow_liked_documents"):
|
||||
follow_document(doctype, name, user)
|
||||
else:
|
||||
if user in liked_by:
|
||||
liked_by.remove(user)
|
||||
|
|
|
|||
|
|
@ -3,10 +3,18 @@
|
|||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
import unittest
|
||||
from dataclasses import dataclass
|
||||
import frappe.desk.form.document_follow as document_follow
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.desk.form.utils import add_comment
|
||||
from frappe.desk.form.document_follow import get_document_followed_by_user
|
||||
from frappe.desk.like import toggle_like
|
||||
from frappe.desk.form.assign_to import add
|
||||
from frappe.share import add as share
|
||||
from frappe.query_builder.functions import Cast_
|
||||
|
||||
class TestDocumentFollow(unittest.TestCase):
|
||||
def test_document_follow(self):
|
||||
def test_document_follow_version(self):
|
||||
user = get_user()
|
||||
event_doc = get_event()
|
||||
|
||||
|
|
@ -18,18 +26,173 @@ class TestDocumentFollow(unittest.TestCase):
|
|||
self.assertEqual(doc.user, user.name)
|
||||
|
||||
document_follow.send_hourly_updates()
|
||||
emails = get_emails(event_doc, '%This is a test description for sending mail%')
|
||||
self.assertIsNotNone(emails)
|
||||
|
||||
email_queue_entry_name = frappe.get_all("Email Queue", limit=1)[0].name
|
||||
email_queue_entry_doc = frappe.get_doc("Email Queue", email_queue_entry_name)
|
||||
|
||||
self.assertEqual((email_queue_entry_doc.recipients[0].recipient), user.name)
|
||||
def test_document_follow_comment(self):
|
||||
user = get_user()
|
||||
event_doc = get_event()
|
||||
|
||||
self.assertIn(event_doc.doctype, email_queue_entry_doc.message)
|
||||
self.assertIn(event_doc.name, email_queue_entry_doc.message)
|
||||
add_comment(event_doc.doctype, event_doc.name, "This is a test comment", 'Administrator@example.com', 'Bosh')
|
||||
|
||||
document_follow.unfollow_document("Event", event_doc.name, user.name)
|
||||
doc = document_follow.follow_document("Event", event_doc.name, user.name)
|
||||
self.assertEqual(doc.user, user.name)
|
||||
|
||||
document_follow.send_hourly_updates()
|
||||
emails = get_emails(event_doc, '%This is a test comment%')
|
||||
self.assertIsNotNone(emails)
|
||||
|
||||
|
||||
def test_follow_limit(self):
|
||||
user = get_user()
|
||||
for _ in range(25):
|
||||
event_doc = get_event()
|
||||
document_follow.unfollow_document("Event", event_doc.name, user.name)
|
||||
doc = document_follow.follow_document("Event", event_doc.name, user.name)
|
||||
self.assertEqual(doc.user, user.name)
|
||||
self.assertEqual(len(get_document_followed_by_user(user.name)), 20)
|
||||
|
||||
def test_follow_on_create(self):
|
||||
user = get_user(DocumentFollowConditions(1))
|
||||
frappe.set_user(user.name)
|
||||
event = get_event()
|
||||
|
||||
event.description = "This is a test description for sending mail"
|
||||
event.save(ignore_version=False)
|
||||
|
||||
documents_followed = get_events_followed_by_user(event.name, user.name)
|
||||
self.assertTrue(documents_followed)
|
||||
|
||||
def test_do_not_follow_on_create(self):
|
||||
user = get_user()
|
||||
frappe.set_user(user.name)
|
||||
|
||||
event = get_event()
|
||||
|
||||
documents_followed = get_events_followed_by_user(event.name, user.name)
|
||||
self.assertFalse(documents_followed)
|
||||
|
||||
def test_do_not_follow_on_update(self):
|
||||
user = get_user()
|
||||
frappe.set_user(user.name)
|
||||
event = get_event()
|
||||
|
||||
event.description = "This is a test description for sending mail"
|
||||
event.save(ignore_version=False)
|
||||
|
||||
documents_followed = get_events_followed_by_user(event.name, user.name)
|
||||
self.assertFalse(documents_followed)
|
||||
|
||||
def test_follow_on_comment(self):
|
||||
user = get_user(DocumentFollowConditions(0, 1))
|
||||
frappe.set_user(user.name)
|
||||
event = get_event()
|
||||
|
||||
add_comment(event.doctype, event.name, "This is a test comment", 'Administrator@example.com', 'Bosh')
|
||||
|
||||
documents_followed = get_events_followed_by_user(event.name, user.name)
|
||||
self.assertTrue(documents_followed)
|
||||
|
||||
def test_do_not_follow_on_comment(self):
|
||||
user = get_user()
|
||||
frappe.set_user(user.name)
|
||||
event = get_event()
|
||||
|
||||
add_comment(event.doctype, event.name, "This is a test comment", 'Administrator@example.com', 'Bosh')
|
||||
|
||||
documents_followed = get_events_followed_by_user(event.name, user.name)
|
||||
self.assertFalse(documents_followed)
|
||||
|
||||
def test_follow_on_like(self):
|
||||
user = get_user(DocumentFollowConditions(0, 0, 1))
|
||||
frappe.set_user(user.name)
|
||||
event = get_event()
|
||||
|
||||
toggle_like(event.doctype, event.name, add="Yes")
|
||||
|
||||
documents_followed = get_events_followed_by_user(event.name, user.name)
|
||||
self.assertTrue(documents_followed)
|
||||
|
||||
def test_do_not_follow_on_like(self):
|
||||
user = get_user()
|
||||
frappe.set_user(user.name)
|
||||
event = get_event()
|
||||
|
||||
toggle_like(event.doctype, event.name)
|
||||
|
||||
documents_followed = get_events_followed_by_user(event.name, user.name)
|
||||
self.assertFalse(documents_followed)
|
||||
|
||||
def test_follow_on_assign(self):
|
||||
user = get_user(DocumentFollowConditions(0, 0, 0, 1))
|
||||
event = get_event()
|
||||
|
||||
add({
|
||||
'assign_to': [user.name],
|
||||
'doctype': event.doctype,
|
||||
'name': event.name
|
||||
})
|
||||
|
||||
documents_followed = get_events_followed_by_user(event.name, user.name)
|
||||
self.assertTrue(documents_followed)
|
||||
|
||||
def test_do_not_follow_on_assign(self):
|
||||
user = get_user()
|
||||
frappe.set_user(user.name)
|
||||
event = get_event()
|
||||
|
||||
add({
|
||||
'assign_to': [user.name],
|
||||
'doctype': event.doctype,
|
||||
'name': event.name
|
||||
})
|
||||
|
||||
documents_followed = get_events_followed_by_user(event.name, user.name)
|
||||
self.assertFalse(documents_followed)
|
||||
|
||||
def test_follow_on_share(self):
|
||||
user = get_user(DocumentFollowConditions(0, 0, 0, 0, 1))
|
||||
event = get_event()
|
||||
|
||||
share(
|
||||
user= user.name,
|
||||
doctype= event.doctype,
|
||||
name= event.name
|
||||
)
|
||||
|
||||
documents_followed = get_events_followed_by_user(event.name, user.name)
|
||||
self.assertTrue(documents_followed)
|
||||
|
||||
def test_do_not_follow_on_share(self):
|
||||
user = get_user()
|
||||
event = get_event()
|
||||
|
||||
share(
|
||||
user = user.name,
|
||||
doctype = event.doctype,
|
||||
name = event.name
|
||||
)
|
||||
|
||||
documents_followed = get_events_followed_by_user(event.name, user.name)
|
||||
self.assertFalse(documents_followed)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.db.delete('Email Queue')
|
||||
frappe.db.delete('Email Queue Recipient')
|
||||
frappe.db.delete('Document Follow')
|
||||
frappe.db.delete('Event')
|
||||
|
||||
def get_events_followed_by_user(event_name, user_name):
|
||||
DocumentFollow = DocType('Document Follow')
|
||||
return (frappe.qb.from_(DocumentFollow)
|
||||
.where(DocumentFollow.ref_doctype == 'Event')
|
||||
.where(DocumentFollow.ref_docname == event_name)
|
||||
.where(DocumentFollow.user == user_name)
|
||||
.select(DocumentFollow.name)).run()
|
||||
|
||||
def get_event():
|
||||
doc = frappe.get_doc({
|
||||
|
|
@ -42,16 +205,40 @@ def get_event():
|
|||
doc.insert()
|
||||
return doc
|
||||
|
||||
def get_user():
|
||||
def get_user(document_follow=None):
|
||||
frappe.set_user("Administrator")
|
||||
if frappe.db.exists('User', 'test@docsub.com'):
|
||||
doc = frappe.get_doc('User', 'test@docsub.com')
|
||||
else:
|
||||
doc = frappe.new_doc("User")
|
||||
doc.email = "test@docsub.com"
|
||||
doc.first_name = "Test"
|
||||
doc.last_name = "User"
|
||||
doc.send_welcome_email = 0
|
||||
doc.document_follow_notify = 1
|
||||
doc.document_follow_frequency = "Hourly"
|
||||
doc.insert()
|
||||
return doc
|
||||
doc = frappe.delete_doc('User', 'test@docsub.com')
|
||||
doc = frappe.new_doc("User")
|
||||
doc.email = "test@docsub.com"
|
||||
doc.first_name = "Test"
|
||||
doc.last_name = "User"
|
||||
doc.send_welcome_email = 0
|
||||
doc.document_follow_notify = 1
|
||||
doc.document_follow_frequency = "Hourly"
|
||||
doc.__dict__.update(document_follow.__dict__ if document_follow else {})
|
||||
doc.insert()
|
||||
doc.add_roles('System Manager')
|
||||
return doc
|
||||
|
||||
def get_emails(event_doc, search_string):
|
||||
EmailQueue = DocType('Email Queue')
|
||||
EmailQueueRecipient = DocType('Email Queue Recipient')
|
||||
|
||||
return (frappe.qb.from_(EmailQueue)
|
||||
.join(EmailQueueRecipient)
|
||||
.on(EmailQueueRecipient.parent == Cast_(EmailQueue.name, "varchar"))
|
||||
.where(EmailQueueRecipient.recipient == 'test@docsub.com',)
|
||||
.where(EmailQueue.message.like(f'%{event_doc.doctype}%'))
|
||||
.where(EmailQueue.message.like(f'%{event_doc.name}%'))
|
||||
.where(EmailQueue.message.like(search_string))
|
||||
.select(EmailQueue.message)
|
||||
.limit(1)).run()
|
||||
|
||||
@dataclass
|
||||
class DocumentFollowConditions:
|
||||
follow_created_documents: int = 0
|
||||
follow_commented_documents: int = 0
|
||||
follow_liked_documents: int = 0
|
||||
follow_assigned_documents: int = 0
|
||||
follow_shared_documents: int = 0
|
||||
|
|
|
|||
|
|
@ -276,7 +276,8 @@ class Document(BaseDocument):
|
|||
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)
|
||||
if frappe.get_cached_value("User", frappe.session.user, "follow_created_documents"):
|
||||
follow_document(self.doctype, self.name, frappe.session.user)
|
||||
return self
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
|
@ -1125,7 +1126,8 @@ class Document(BaseDocument):
|
|||
version.insert(ignore_permissions=True)
|
||||
if not frappe.flags.in_migrate:
|
||||
# follow since you made a change?
|
||||
follow_document(self.doctype, self.name, frappe.session.user)
|
||||
if frappe.get_cached_value("User", frappe.session.user, "follow_created_documents"):
|
||||
follow_document(self.doctype, self.name, frappe.session.user)
|
||||
|
||||
@staticmethod
|
||||
def hook(f):
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ def make_boilerplate(template, doc, opts=None):
|
|||
frappe.utils.cstr(source.read()).format(
|
||||
app_publisher=app_publisher,
|
||||
year=frappe.utils.nowdate()[:4],
|
||||
classname=doc.name.replace(" ", ""),
|
||||
classname=doc.name.replace(" ", "").replace("-", ""),
|
||||
base_class_import=base_class_import,
|
||||
base_class=base_class,
|
||||
doctype=doc.name, **opts,
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ frappe.patches.v13_0.update_duration_options
|
|||
frappe.patches.v13_0.replace_old_data_import # 2020-06-24
|
||||
frappe.patches.v13_0.create_custom_dashboards_cards_and_charts
|
||||
frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart
|
||||
frappe.patches.v13_0.add_standard_navbar_items # 2022-03-15
|
||||
frappe.patches.v13_0.add_standard_navbar_items # 2020-12-15
|
||||
frappe.patches.v13_0.generate_theme_files_in_public_folder
|
||||
frappe.patches.v13_0.increase_password_length
|
||||
frappe.patches.v12_0.fix_email_id_formatting
|
||||
|
|
|
|||
|
|
@ -1701,13 +1701,17 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
}
|
||||
|
||||
update_in_all_rows(table_fieldname, fieldname, value) {
|
||||
// update the child value in all tables where it is missing
|
||||
if(!value) return;
|
||||
var cl = this.doc[table_fieldname] || [];
|
||||
for(var i = 0; i < cl.length; i++){
|
||||
if(!cl[i][fieldname]) cl[i][fieldname] = value;
|
||||
}
|
||||
refresh_field("items");
|
||||
// Update the `value` of the field named `fieldname` in all rows of the
|
||||
// child table named `table_fieldname`.
|
||||
// Do not overwrite existing values.
|
||||
if (value === undefined) return;
|
||||
|
||||
frappe.model
|
||||
.get_children(this.doc, table_fieldname)
|
||||
.filter(child => !frappe.model.has_value(child.doctype, child.name, fieldname))
|
||||
.forEach(child =>
|
||||
frappe.model.set_value(child.doctype, child.name, fieldname, value)
|
||||
);
|
||||
}
|
||||
|
||||
get_sum(table_fieldname, fieldname) {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ export default class WebForm extends frappe.ui.FieldGroup {
|
|||
this.setup_listeners();
|
||||
if (this.introduction_text) this.set_form_description(this.introduction_text);
|
||||
if (this.allow_print && !this.is_new) this.setup_print_button();
|
||||
if (this.allow_delete && !this.is_new) this.setup_delete_button();
|
||||
if (this.is_new) this.setup_cancel_button();
|
||||
this.setup_primary_action();
|
||||
this.setup_previous_next_button();
|
||||
|
|
@ -79,9 +78,9 @@ export default class WebForm extends frappe.ui.FieldGroup {
|
|||
}
|
||||
|
||||
$('.web-form-footer').after(`
|
||||
<div id="form-step-footer" class="pull-right">
|
||||
<button class="btn btn-primary btn-previous btn-sm ml-2">${__("Previous")}</button>
|
||||
<button class="btn btn-primary btn-next btn-sm ml-2">${__("Next")}</button>
|
||||
<div id="form-step-footer" class="text-right">
|
||||
<button class="btn btn-default btn-previous btn-sm ml-2">${__("Previous")}</button>
|
||||
<button class="btn btn-default btn-next btn-sm ml-2">${__("Next")}</button>
|
||||
</div>
|
||||
`);
|
||||
|
||||
|
|
@ -141,6 +140,7 @@ export default class WebForm extends frappe.ui.FieldGroup {
|
|||
set_form_description(intro) {
|
||||
let intro_wrapper = document.getElementById('introduction');
|
||||
intro_wrapper.innerHTML = intro;
|
||||
intro_wrapper.classList.remove('hidden');
|
||||
}
|
||||
|
||||
add_button(name, type, action, wrapper_class=".web-form-actions") {
|
||||
|
|
@ -164,25 +164,18 @@ export default class WebForm extends frappe.ui.FieldGroup {
|
|||
this.save()
|
||||
);
|
||||
|
||||
this.add_button_to_footer(this.button_label || __("Save", null, "Button in web form"), "primary", () =>
|
||||
this.save()
|
||||
);
|
||||
if (!this.is_multi_step_form && $('.frappe-card').height() > 600) {
|
||||
// add button on footer if page is long
|
||||
this.add_button_to_footer(this.button_label || __("Save", null, "Button in web form"), "primary", () =>
|
||||
this.save()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setup_cancel_button() {
|
||||
this.add_button_to_header(__("Cancel", null, "Button in web form"), "light", () => this.cancel());
|
||||
}
|
||||
|
||||
setup_delete_button() {
|
||||
frappe.has_permission(this.doc_type, "", "delete", () => {
|
||||
this.add_button_to_header(
|
||||
frappe.utils.icon('delete'),
|
||||
"danger",
|
||||
() => this.delete()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
setup_print_button() {
|
||||
this.add_button_to_header(
|
||||
frappe.utils.icon('print'),
|
||||
|
|
@ -359,17 +352,6 @@ export default class WebForm extends frappe.ui.FieldGroup {
|
|||
return true;
|
||||
}
|
||||
|
||||
delete() {
|
||||
frappe.call({
|
||||
type: "POST",
|
||||
method: "frappe.website.doctype.web_form.web_form.delete",
|
||||
args: {
|
||||
web_form_name: this.name,
|
||||
docname: this.doc.name
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
print() {
|
||||
window.open(`/printview?
|
||||
doctype=${this.doc_type}
|
||||
|
|
@ -386,21 +368,19 @@ export default class WebForm extends frappe.ui.FieldGroup {
|
|||
window.location.href = data;
|
||||
}
|
||||
|
||||
const success_dialog = new frappe.ui.Dialog({
|
||||
title: __("Saved Successfully"),
|
||||
secondary_action: () => {
|
||||
if (this.success_url) {
|
||||
window.location.href = this.success_url;
|
||||
} else if(this.login_required) {
|
||||
window.location.href =
|
||||
window.location.pathname + "?name=" + data.name;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
success_dialog.show();
|
||||
const success_message =
|
||||
this.success_message || __("Your information has been submitted");
|
||||
success_dialog.set_message(success_message);
|
||||
this.success_message || __("Submitted");
|
||||
|
||||
frappe.toast({message: success_message, indicator:'green'});
|
||||
|
||||
// redirect
|
||||
setTimeout(() => {
|
||||
if (this.success_url) {
|
||||
window.location.href = this.success_url;
|
||||
} else if(this.login_required) {
|
||||
window.location.href =
|
||||
window.location.pathname + "?name=" + data.name;
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default class WebFormList {
|
|||
constructor(opts) {
|
||||
Object.assign(this, opts);
|
||||
frappe.web_form_list = this;
|
||||
this.wrapper = document.getElementById("datatable");
|
||||
this.wrapper = document.getElementById("list-table");
|
||||
this.make_actions();
|
||||
this.make_filters();
|
||||
$('.link-btn').remove();
|
||||
|
|
@ -320,6 +320,7 @@ frappe.ui.WebFormListRow = class WebFormListRow {
|
|||
make_row() {
|
||||
// Add Checkboxes
|
||||
let cell = this.row.insertCell();
|
||||
cell.classList.add('list-col-checkbox');
|
||||
|
||||
this.checkbox = document.createElement("input");
|
||||
this.checkbox.type = "checkbox";
|
||||
|
|
@ -332,6 +333,7 @@ frappe.ui.WebFormListRow = class WebFormListRow {
|
|||
|
||||
// Add Serial Number
|
||||
let serialNo = this.row.insertCell();
|
||||
serialNo.classList.add('list-col-serial');
|
||||
serialNo.innerText = this.serial_number;
|
||||
|
||||
this.columns.forEach(field => {
|
||||
|
|
|
|||
|
|
@ -52,11 +52,11 @@ frappe.ready(function() {
|
|||
const data = setup_fields(r.message);
|
||||
let web_form_doc = data.web_form;
|
||||
|
||||
if (web_form_doc.name && web_form_doc.allow_edit === 0) {
|
||||
if (!window.location.href.includes("?new=1")) {
|
||||
window.location.replace(window.location.pathname + "?new=1");
|
||||
}
|
||||
}
|
||||
// if (web_form_doc.name && web_form_doc.allow_edit === 0) {
|
||||
// if (!window.location.href.includes("?new=1")) {
|
||||
// window.location.replace(window.location.pathname + "?new=1");
|
||||
// }
|
||||
// }
|
||||
let doc = r.message.doc || build_doc(r.message);
|
||||
web_form.prepare(web_form_doc, r.message.doc && web_form_doc.allow_edit === 1 ? r.message.doc : {});
|
||||
web_form.make();
|
||||
|
|
|
|||
|
|
@ -111,8 +111,8 @@
|
|||
}
|
||||
|
||||
.avatar-large {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
|
||||
.standard-image {
|
||||
font-size: var(--text-2xl);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,13 @@
|
|||
$font-size-xs: 0.7rem;
|
||||
$font-size-sm: 0.85rem;
|
||||
$font-size-lg: 1.12rem;
|
||||
$font-size-xl: 1.25rem;
|
||||
$font-size-2xl: 1.5rem;
|
||||
$font-size-3xl: 2rem;
|
||||
$font-size-4xl: 2.5rem;
|
||||
$font-size-5xl: 3rem;
|
||||
$font-size-6xl: 4rem;
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
|
@ -14,45 +24,80 @@ img {
|
|||
height: auto;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: $font-size-3xl;
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
letter-spacing: -0.025em;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
font-size: $font-size-5xl;
|
||||
line-height: 2.5rem;
|
||||
font-size: $font-size-4xl;
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
@include media-breakpoint-up(xl) {
|
||||
font-size: $font-size-6xl;
|
||||
line-height: 1;
|
||||
font-size: $font-size-5xl;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $font-size-xl;
|
||||
font-weight: 700;
|
||||
font-size: $font-size-2xl;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
font-size: $font-size-2xl;
|
||||
}
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: $font-size-3xl;
|
||||
margin-top: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@include media-breakpoint-up(xl) {
|
||||
font-size: $font-size-4xl;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: $font-size-base;
|
||||
font-weight: 600;
|
||||
font-size: $font-size-xl;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
font-size: $font-size-lg;
|
||||
font-size: $font-size-2xl;
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: $font-size-xl;
|
||||
@include media-breakpoint-up(xl) {
|
||||
font-size: $font-size-3xl;
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: $font-size-lg;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
font-size: $font-size-xl;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
@include media-breakpoint-up(xl) {
|
||||
font-size: $font-size-2xl;
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $body-color;
|
||||
}
|
||||
}
|
||||
|
||||
.btn.btn-lg {
|
||||
font-size: $font-size-lg;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,12 +57,12 @@
|
|||
|
||||
.blog-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: top;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
.avatar {
|
||||
margin-top: 0.4rem;
|
||||
margin-right: 0.5rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -119,106 +119,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-comment-button {
|
||||
margin-left: 35px;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 22px;
|
||||
background-color: var(--fg-color);
|
||||
border: 1px solid var(--dark-border-color);
|
||||
|
||||
&:before {
|
||||
content: ' ';
|
||||
background: var(--gray-600);
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
border-radius: 50%;
|
||||
height: 4px;
|
||||
width: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.blog-comments {
|
||||
.comment-form-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.add-comment-section {
|
||||
.login-required {
|
||||
padding: var(--padding-sm);
|
||||
border-radius: var(--border-radius-sm);
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
.new-comment {
|
||||
display: flex;
|
||||
padding: var(--padding-lg);
|
||||
box-shadow: var(--card-shadow);
|
||||
border-radius: var(--border-radius-md);
|
||||
|
||||
.new-comment-fields {
|
||||
flex: 1;
|
||||
|
||||
.form-label {
|
||||
font-weight: var(--text-bold);
|
||||
}
|
||||
|
||||
.comment-text-area textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.comment-by {
|
||||
padding-right: 0px !important;
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#comment-list {
|
||||
position: relative;
|
||||
padding-left: var(--padding-xl);
|
||||
|
||||
&:before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
top: var(--comment-timeline-top);
|
||||
bottom: var(--comment-timeline-bottom);
|
||||
border-left: 1px solid var(--dark-border-color);
|
||||
}
|
||||
|
||||
.comment-row {
|
||||
position: relative;
|
||||
|
||||
.comment-avatar {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: -17px;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
box-shadow: var(--card-shadow);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: var(--padding-md);
|
||||
margin-left: 35px;
|
||||
flex: 1;
|
||||
|
||||
.content p{
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
.web-footer {
|
||||
padding: 5rem 0;
|
||||
margin: 5rem 0;
|
||||
min-height: 140px;
|
||||
background-color: var(--fg-color);
|
||||
border-top: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
|
|
@ -76,8 +78,6 @@
|
|||
}
|
||||
|
||||
.footer-info {
|
||||
margin-top: 1rem;
|
||||
border-top: 1px solid $border-color;
|
||||
color: $text-muted;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
|
@ -98,4 +98,4 @@
|
|||
font-size: $font-size-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
@import "../common/global";
|
||||
@import "../common/icons";
|
||||
@import "../common/alert";
|
||||
@import 'base';
|
||||
@import "../common/flex";
|
||||
@import "../common/buttons";
|
||||
@import "../common/modal";
|
||||
|
|
@ -14,6 +13,7 @@
|
|||
@import "../common/indicator";
|
||||
@import "../common/controls";
|
||||
@import "../common/awesomeplete";
|
||||
@import 'base';
|
||||
@import 'multilevel_dropdown';
|
||||
@import 'website_image';
|
||||
@import 'website_avatar';
|
||||
|
|
|
|||
|
|
@ -1,30 +1,12 @@
|
|||
$font-sizes-desktop: (
|
||||
"sm": 0.75rem,
|
||||
"base": 1rem,
|
||||
"lg": 1.125rem,
|
||||
"xl": 1.41rem,
|
||||
"2xl": 1.6rem,
|
||||
"3xl": 2rem
|
||||
);
|
||||
|
||||
$font-sizes-mobile: (
|
||||
"sm": 0.75rem,
|
||||
"base": 1rem,
|
||||
"lg": 1.125rem,
|
||||
"xl": 1.25rem,
|
||||
"2xl": 1.5rem,
|
||||
"3xl": 1.75rem
|
||||
);
|
||||
.section-markdown > .from-markdown {
|
||||
max-width: 50rem;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.from-markdown {
|
||||
color: $gray-700;
|
||||
line-height: 1.7;
|
||||
letter-spacing: -0.011em;
|
||||
|
||||
> * + * {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
> :first-child {
|
||||
margin-top: 0;
|
||||
|
|
@ -47,6 +29,10 @@ $font-sizes-mobile: (
|
|||
list-style: decimal;
|
||||
}
|
||||
|
||||
p, li {
|
||||
font-size: $font-size-lg;
|
||||
}
|
||||
|
||||
li {
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
|
|
@ -87,86 +73,6 @@ $font-sizes-mobile: (
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: $gray-900;
|
||||
}
|
||||
|
||||
h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: map-get($font-sizes-mobile, '3xl');
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.021em;
|
||||
font-weight: 700;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: map-get($font-sizes-desktop, '3xl');
|
||||
letter-spacing: -0.024em;
|
||||
}
|
||||
|
||||
// for byline
|
||||
& + p {
|
||||
margin-top: 1.5rem;
|
||||
font-size: map-get($font-sizes-mobile, 'xl');
|
||||
letter-spacing: -0.014em;
|
||||
line-height: 1.4;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: map-get($font-sizes-desktop, 'xl');
|
||||
letter-spacing: -0.0175em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: map-get($font-sizes-mobile, '2xl');
|
||||
line-height: 1.56;
|
||||
letter-spacing: -0.015em;
|
||||
margin-top: 4rem;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: map-get($font-sizes-desktop, '2xl');
|
||||
letter-spacing: -0.0195em;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: map-get($font-sizes-mobile, 'xl');
|
||||
line-height: 1.56;
|
||||
letter-spacing: -0.014em;
|
||||
margin-top: 2.25rem;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: map-get($font-sizes-desktop, 'xl');
|
||||
letter-spacing: -0.0175em;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: map-get($font-sizes-mobile, 'lg');
|
||||
line-height: 1.56;
|
||||
letter-spacing: -0.014em;
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: map-get($font-sizes-mobile, 'base');
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.011em;
|
||||
font-weight: 600;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: map-get($font-sizes-mobile, 'sm');
|
||||
line-height: 1.35;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
tr > td,
|
||||
tr > th {
|
||||
font-size: $font-size-sm;
|
||||
|
|
|
|||
|
|
@ -27,15 +27,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.my-account-container {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.account-info {
|
||||
background-color: var(--fg-color);
|
||||
box-shadow: var(--card-shadow);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: var(--padding-sm) 25px;
|
||||
max-width: 850px;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
padding: 0;
|
||||
|
|
@ -97,21 +98,3 @@
|
|||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
//styles for third party apps page
|
||||
//center wrt to outer most container and not immediate parent
|
||||
.empty-apps-state {
|
||||
position: relative;
|
||||
padding-top: 10rem;
|
||||
margin-left: -250px;
|
||||
text-align: center;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
margin: auto;
|
||||
padding-top: 5rem;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
.hero-content {
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
.btn-primary {
|
||||
margin-top: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
|
|
@ -15,16 +18,23 @@
|
|||
|
||||
.hero-title, .hero-subtitle {
|
||||
max-width: 42rem;
|
||||
margin-top: 0rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-weight: normal;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
@extend .lead;
|
||||
font-weight: 400;
|
||||
color: $gray-600;
|
||||
font-size: 1rem;
|
||||
font-size: $font-size-lg;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
font-size: 1.25rem;
|
||||
font-size: $font-size-xl;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,10 +52,10 @@
|
|||
.section-description {
|
||||
max-width: 56rem;
|
||||
margin-top: 0.5rem;
|
||||
font-size: $font-size-base;
|
||||
font-size: $font-size-lg;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
font-size: $font-size-lg;
|
||||
@include media-breakpoint-up(media-breakpoint-up) {
|
||||
font-size: $font-size-xl;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -226,14 +236,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.section-markdown > .from-markdown {
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.section-cta {
|
||||
padding: 3rem 2rem;
|
||||
text-align: center;
|
||||
background-color: $primary-light;
|
||||
background-color: $gray-200;
|
||||
border-radius: 0.75rem;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
|
|
@ -248,12 +254,7 @@
|
|||
.title {
|
||||
margin: 0 auto;
|
||||
max-width: 36rem;
|
||||
font-size: $font-size-2xl;
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: $font-size-4xl;
|
||||
}
|
||||
}
|
||||
.subtitle {
|
||||
max-width: 36rem;
|
||||
|
|
@ -270,11 +271,15 @@
|
|||
margin-top: 0.5rem;
|
||||
font-size: $font-size-xs;
|
||||
}
|
||||
.action {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-small-cta {
|
||||
padding: 1.8rem;
|
||||
background-color: lighten($primary, 42%);
|
||||
background-color: var(--gray-200);
|
||||
border-radius: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -294,26 +299,27 @@
|
|||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
max-width: 36rem;
|
||||
font-size: $font-size-xl;
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: $font-size-2xl;
|
||||
}
|
||||
.section-title {
|
||||
line-height: 1;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
max-width: 36rem;
|
||||
font-size: $font-size-base;
|
||||
color: $gray-900;
|
||||
margin-bottom: 1.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: $font-size-lg;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-cta-container {
|
||||
|
|
@ -379,6 +385,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
.testimonial-author {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.avatar {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.split-section-content.align-top {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
|
@ -514,12 +534,12 @@
|
|||
|
||||
@include media-breakpoint-up(md) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 6rem;
|
||||
gap: 3rem 5rem;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: $font-size-xl;
|
||||
font-weight: bold;
|
||||
font-size: $font-size-lg;
|
||||
font-weight: 600;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: $font-size-2xl;
|
||||
|
|
@ -528,7 +548,7 @@
|
|||
|
||||
.feature-content {
|
||||
font-size: $font-size-base;
|
||||
margin-top: 1.75rem;
|
||||
margin-top: 1.25rem;
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
font-size: $font-size-lg;
|
||||
|
|
@ -630,9 +650,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.section-title + .section-features, .section-description + .section-features {
|
||||
&[data-columns="2"] {
|
||||
margin-top: 3.75rem;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
&[data-columns="3"] {
|
||||
|
|
@ -651,6 +676,14 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.feature-content {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.feature-title, .feature-content {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
@ -666,3 +699,19 @@
|
|||
.section-with-embed .embed-container {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.section-video-wrapper {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-video {
|
||||
aspect-ratio: 16 / 9;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.video-thumbnail {
|
||||
aspect-ratio: 16 / 9;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ $font-size-lg: 1.125rem !default;
|
|||
$font-size-xl: 1.25rem !default;
|
||||
$font-size-2xl: 1.5rem !default;
|
||||
$font-size-3xl: 1.875rem !default;
|
||||
$font-size-4xl: 2.25rem !default;
|
||||
$font-size-4xl: 2.5rem !default;
|
||||
$font-size-5xl: 3rem !default;
|
||||
$font-size-6xl: 4rem !default;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,47 @@
|
|||
@import "../common/form";
|
||||
|
||||
|
||||
[data-doctype="Web Form"] {
|
||||
.page-content-wrapper {
|
||||
.page_content {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
|
||||
.frappe-card {
|
||||
padding: 1rem;
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.web-form-head {
|
||||
margin: 0 -1rem;
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
#introduction {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
#introduction p {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.web-form-actions button {
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.frappe-card.list-card {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.breadcrumb-container.container {
|
||||
@include media-breakpoint-up(sm) {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
|
||||
&.my-4 {
|
||||
background-color: var(--fg-color);
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
padding: 1.8rem;
|
||||
border-radius: var(--border-radius-md);
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,13 +76,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
.web-form-wrapper~#datatable {
|
||||
.list-table {
|
||||
margin-left: -1rem;
|
||||
margin-right: -1rem;
|
||||
|
||||
.table {
|
||||
thead {
|
||||
th {
|
||||
border: 0;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
color: var(--text-muted)
|
||||
color: var(--text-muted);
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -71,8 +98,22 @@
|
|||
color: var(--text-color);
|
||||
|
||||
td {
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-size: 13px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin-left: 0.5rem;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.list-col-checkbox {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.list-col-serial {
|
||||
width: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,28 @@ CombineDatetime = ImportMapper(
|
|||
)
|
||||
|
||||
|
||||
class Cast_(Function):
|
||||
def __init__(self, value, as_type, alias=None):
|
||||
if db_type_is.MARIADB and (
|
||||
(hasattr(as_type, "get_sql") and as_type.get_sql().lower() == "varchar") or str(as_type).lower() == "varchar"
|
||||
):
|
||||
# mimics varchar cast in mariadb
|
||||
# as mariadb doesn't have varchar data cast
|
||||
# https://mariadb.com/kb/en/cast/#description
|
||||
|
||||
# ref: https://stackoverflow.com/a/32542095
|
||||
super().__init__("CONCAT", value, "", alias=alias)
|
||||
else:
|
||||
# from source: https://pypika.readthedocs.io/en/latest/_modules/pypika/functions.html#Cast
|
||||
super().__init__("CAST", value, alias=alias)
|
||||
self.as_type = as_type
|
||||
|
||||
def get_special_params_sql(self, **kwargs):
|
||||
if self.name.lower() == "cast":
|
||||
type_sql = self.as_type.get_sql(**kwargs) if hasattr(self.as_type, "get_sql") else str(self.as_type).upper()
|
||||
return "AS {type}".format(type=type_sql)
|
||||
|
||||
|
||||
def _aggregate(function, dt, fieldname, filters, **kwargs):
|
||||
return (
|
||||
Query()
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ def patch_query_execute():
|
|||
This excludes the use of `frappe.db.sql` method while
|
||||
executing the query object
|
||||
"""
|
||||
from frappe.utils.safe_exec import check_safe_sql_query
|
||||
|
||||
|
||||
def execute_query(query, *args, **kwargs):
|
||||
query, params = prepare_query(query)
|
||||
return frappe.db.sql(query, params, *args, **kwargs) # nosemgrep
|
||||
|
|
@ -63,7 +66,7 @@ def patch_query_execute():
|
|||
|
||||
param_collector = NamedParameterWrapper()
|
||||
query = query.get_sql(param_wrapper=param_collector)
|
||||
if frappe.flags.in_safe_exec and not query.lower().strip().startswith("select"):
|
||||
if frappe.flags.in_safe_exec and not check_safe_sql_query(query, throw=False):
|
||||
callstack = inspect.stack()
|
||||
if len(callstack) >= 3 and ".py" in callstack[2].filename:
|
||||
# ignore any query builder methods called from python files
|
||||
|
|
@ -77,7 +80,7 @@ def patch_query_execute():
|
|||
#
|
||||
# if frame2 is server script it wont have a filename and hence
|
||||
# it shouldn't be allowed.
|
||||
# ps. stack() returns `"<unknown>"` as filename.
|
||||
# p.s. stack() returns `"<unknown>"` as filename if not a file.
|
||||
pass
|
||||
else:
|
||||
raise frappe.PermissionError('Only SELECT SQL allowed in scripting')
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ def add(doctype, name, user=None, read=1, write=0, submit=0, share=0, everyone=0
|
|||
doc.save(ignore_permissions=True)
|
||||
notify_assignment(user, doctype, name, everyone, notify=notify)
|
||||
|
||||
follow_document(doctype, name, user)
|
||||
if frappe.get_cached_value("User", user, "follow_shared_documents"):
|
||||
follow_document(doctype, name, user)
|
||||
|
||||
return doc
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
{% macro avatar(user_id=None, css_style=None, size="avatar-small") %}
|
||||
{% macro avatar(user_id=None, css_style=None, size="avatar-small", full_name=None, image=None) %}
|
||||
{% set user_info = frappe.utils.get_user_info_for_avatar(user_id) %}
|
||||
<span class="avatar {{ size }}" title="{{ user_info.name }}" style="{{ css_style or '' }}">
|
||||
{% if user_info.image %}
|
||||
<span class="avatar {{ size }}" title="{{ full_name or user_info.name }}" style="{{ css_style or '' }}">
|
||||
{% if image or user_info.image %}
|
||||
<img
|
||||
class="avatar-frame standard-image"
|
||||
src="{{ user_info.image }}"
|
||||
title="{{ user_info.name }}">
|
||||
src="{{ image or user_info.image }}"
|
||||
title="{{ full_name or user_info.name }}">
|
||||
</span>
|
||||
{% else %}
|
||||
<span
|
||||
class="avatar-frame standard-image"
|
||||
title="{{ user_info.name }}">
|
||||
{{ frappe.utils.get_abbr(user_info.name).upper() }}
|
||||
title="{{ full_name or user_info.name }}">
|
||||
{{ frappe.utils.get_abbr(full_name or user_info.name).upper() }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endmacro %}
|
||||
{% endmacro %}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
{% from "frappe/templates/includes/macros.html" import square_image_with_fallback %}
|
||||
{% from "frappe/templates/includes/avatar_macro.html" import avatar %}
|
||||
|
||||
<div class="media">
|
||||
{{ square_image_with_fallback(src=blogger_info.avatar, size='small', alt=blogger_info.full_name, class='align-self-start mr-4 rounded') }}
|
||||
<div class="media-body">
|
||||
{{ avatar(full_name=blogger_info.full_name, image=blogger_info.avatar, size='avatar-large') }}
|
||||
|
||||
<div class="media-body ml-3">
|
||||
<h5 class="mt-0">
|
||||
<a href="/blog?blogger={{ blogger_info.name }}" class="text-dark">{{ blogger_info.full_name }}</a>
|
||||
</h5>
|
||||
|
|
|
|||
|
|
@ -62,11 +62,13 @@
|
|||
let user_id = "";
|
||||
|
||||
let update_timeline_line_length = function(direction, size) {
|
||||
if (direction == 'top') {
|
||||
$('.blog-container')[0].style.setProperty('--comment-timeline-top', size);
|
||||
} else {
|
||||
let comment_timeline_bottom = $('.comment-list .comment-row:last-child').height() - 10;
|
||||
$('.blog-container')[0].style.setProperty('--comment-timeline-bottom', comment_timeline_bottom +'px');
|
||||
if ($('.blog-container').length) {
|
||||
if (direction == 'top') {
|
||||
$('.blog-container')[0].style.setProperty('--comment-timeline-top', size);
|
||||
} else {
|
||||
let comment_timeline_bottom = $('.comment-list .comment-row:last-child').height() - 10;
|
||||
$('.blog-container')[0].style.setProperty('--comment-timeline-bottom', comment_timeline_bottom +'px');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -194,3 +196,105 @@
|
|||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
.add-comment-button {
|
||||
margin-left: 35px;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 22px;
|
||||
background-color: var(--fg-color);
|
||||
border: 1px solid var(--dark-border-color);
|
||||
}
|
||||
|
||||
.timeline-dot::before {
|
||||
content: ' ';
|
||||
background: var(--gray-600);
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
border-radius: 50%;
|
||||
height: 4px;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.comment-form-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-required {
|
||||
padding: var(--padding-sm);
|
||||
border-radius: var(--border-radius-sm);
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
.new-comment {
|
||||
display: flex;
|
||||
padding: var(--padding-lg);
|
||||
box-shadow: var(--card-shadow);
|
||||
border-radius: var(--border-radius-md);
|
||||
background-color: var(--fg-color);
|
||||
}
|
||||
|
||||
.new-comment-fields {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.new-comment .form-label {
|
||||
font-weight: var(--text-bold);
|
||||
}
|
||||
|
||||
.new-comment .comment-text-area textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.comment-by {
|
||||
padding-right: 0px !important;
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
#comment-list {
|
||||
position: relative;
|
||||
padding-left: var(--padding-xl);
|
||||
}
|
||||
|
||||
#comment-list::before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
top: var(--comment-timeline-top);
|
||||
bottom: var(--comment-timeline-bottom);
|
||||
border-left: 1px solid var(--dark-border-color);
|
||||
}
|
||||
|
||||
.comment-row {
|
||||
position: relative;
|
||||
}
|
||||
.comment-avatar {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: -17px;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
box-shadow: var(--card-shadow);
|
||||
background-color: var(--fg-color);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: var(--padding-md);
|
||||
margin-left: 35px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.comment-content .content p{
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -313,7 +313,7 @@ var continue_otp_app = function (setup, qrcode) {
|
|||
var qrcode_div = $('<div class="text-muted" style="padding-bottom: 15px;"></div>');
|
||||
|
||||
if (setup) {
|
||||
direction = $('<div>').attr('id', 'qr_info').text('{{ _("Enter Code displayed in OTP App.") }}');
|
||||
direction = $('<div>').attr('id', 'qr_info').html('{{ _("Enter Code displayed in OTP App.") }}');
|
||||
qrcode_div.append(direction);
|
||||
$('#otp_div').prepend(qrcode_div);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
'section-padding-top': web_block.add_top_padding,
|
||||
'section-padding-bottom': web_block.add_bottom_padding,
|
||||
'bg-light': web_block.add_shade,
|
||||
'border-top': web_block.add_border_at_top,
|
||||
'border-bottom': web_block.add_border_at_bottom,
|
||||
},
|
||||
web_block.css_class
|
||||
]) -%}
|
||||
|
|
@ -10,7 +12,10 @@
|
|||
{%- if web_template_type == 'Section' -%}
|
||||
{%- if not web_block.hide_block -%}
|
||||
<section class="section {{ classes }}" data-section-idx="{{ web_block.idx | e }}"
|
||||
data-section-template="{{ web_block.web_template | e }}">
|
||||
data-section-template="{{ web_block.web_template | e }}"
|
||||
{% if web_block.add_background_image -%}
|
||||
style="background: url({{ web_block.background_image}}) no-repeat center center; background-size: cover;"
|
||||
{%- endif %}>
|
||||
{%- if web_block.add_container -%}
|
||||
<div class="container">
|
||||
{%- endif -%}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ class TestGlobalSearch(unittest.TestCase):
|
|||
|
||||
def test_delete_doc(self):
|
||||
self.insert_test_events()
|
||||
|
||||
event_name = frappe.get_all('Event')[0].name
|
||||
event = frappe.get_doc('Event', event_name)
|
||||
test_subject = event.subject
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from typing import Callable
|
|||
|
||||
import frappe
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Coalesce, GroupConcat, Match, CombineDatetime
|
||||
from frappe.query_builder.functions import Coalesce, GroupConcat, Match, CombineDatetime, Cast_
|
||||
from frappe.query_builder.utils import db_type_is
|
||||
from frappe.query_builder import Case
|
||||
|
||||
|
|
@ -53,6 +53,11 @@ class TestCustomFunctionsMariaDB(unittest.TestCase):
|
|||
select_query = select_query.select(CombineDatetime(note.posting_date, note.posting_time, alias="timestamp"))
|
||||
self.assertIn("timestamp(`tabnote`.`posting_date`,`tabnote`.`posting_time`) `timestamp`", str(select_query).lower())
|
||||
|
||||
def test_cast(self):
|
||||
note = frappe.qb.DocType("Note")
|
||||
self.assertEqual("CONCAT(`tabnote`.`name`, '')", Cast_(note.name, "varchar"))
|
||||
self.assertEqual("CAST(`tabnote`.`name` AS INTEGER)", Cast_(note.name, "integer"))
|
||||
|
||||
|
||||
@run_only_if(db_type_is.POSTGRES)
|
||||
class TestCustomFunctionsPostgres(unittest.TestCase):
|
||||
|
|
@ -97,6 +102,11 @@ class TestCustomFunctionsPostgres(unittest.TestCase):
|
|||
select_query = select_query.select(CombineDatetime(note.posting_date, note.posting_time, alias="timestamp"))
|
||||
self.assertIn('"tabnote"."posting_date"+"tabnote"."posting_time" "timestamp"', str(select_query).lower())
|
||||
|
||||
def test_cast(self):
|
||||
note = frappe.qb.DocType("Note")
|
||||
self.assertEqual("CAST(`tabnote`.`name` AS VARCHAR)", Cast_(note.name, "varchar"))
|
||||
self.assertEqual("CAST(`tabnote`.`name` AS INTEGER)", Cast_(note.name, "integer"))
|
||||
|
||||
|
||||
class TestBuilderBase(object):
|
||||
def test_adding_tabs(self):
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ def add_standard_navbar_items():
|
|||
'is_standard': 1
|
||||
},
|
||||
{
|
||||
'item_label': 'Logout',
|
||||
'item_label': 'Log out',
|
||||
'item_type': 'Action',
|
||||
'action': 'frappe.app.logout()',
|
||||
'is_standard': 1
|
||||
|
|
|
|||
|
|
@ -269,11 +269,33 @@ def get_hooks(hook=None, default=None, app_name=None):
|
|||
def read_sql(query, *args, **kwargs):
|
||||
'''a wrapper for frappe.db.sql to allow reads'''
|
||||
query = str(query)
|
||||
if frappe.flags.in_safe_exec and not query.strip().lower().startswith('select'):
|
||||
raise frappe.PermissionError('Only SELECT SQL allowed in scripting')
|
||||
if frappe.flags.in_safe_exec:
|
||||
check_safe_sql_query(query)
|
||||
return frappe.db.sql(query, *args, **kwargs)
|
||||
|
||||
|
||||
def check_safe_sql_query(query: str, throw: bool = True) -> bool:
|
||||
""" Check if SQL query is safe for running in restricted context.
|
||||
|
||||
Safe queries:
|
||||
1. Read only 'select' or 'explain' queries
|
||||
2. CTE on mariadb where writes are not allowed.
|
||||
"""
|
||||
|
||||
query = query.strip().lower()
|
||||
whitelisted_statements = ("select", "explain")
|
||||
|
||||
if (query.startswith(whitelisted_statements)
|
||||
or (query.startswith("with") and frappe.db.db_type == "mariadb")):
|
||||
return True
|
||||
|
||||
if throw:
|
||||
frappe.throw(_("Query must be of SELECT or read-only WITH type."),
|
||||
title=_("Unsafe SQL query"), exc=frappe.PermissionError)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _getitem(obj, key):
|
||||
# guard function for RestrictedPython
|
||||
# allow any key to be accessed as long as it does not start with underscore
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@
|
|||
"depends_on": "eval:doc.content_type === 'Markdown'",
|
||||
"fieldname": "content_md",
|
||||
"fieldtype": "Markdown Editor",
|
||||
"ignore_xss_filter": 1,
|
||||
"label": "Content (Markdown)"
|
||||
},
|
||||
{
|
||||
|
|
@ -213,7 +214,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"is_published_field": "published",
|
||||
"links": [],
|
||||
"modified": "2022-03-09 01:48:25.227295",
|
||||
"modified": "2022-03-21 14:42:19.282612",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Blog Post",
|
||||
|
|
@ -245,6 +246,7 @@
|
|||
"route": "blog",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
{% from "frappe/templates/includes/avatar_macro.html" import avatar %}
|
||||
|
||||
{%- set post = doc -%}
|
||||
<div class="blog-card col-sm-12 {{ 'col-md-8' if post.featured else 'col-md-4' }}">
|
||||
<div class="card h-100">
|
||||
|
|
@ -26,7 +28,7 @@
|
|||
<p class="post-description text-muted">{{ post.intro }}</p>
|
||||
</div>
|
||||
<div class="blog-card-footer">
|
||||
<img class="avatar website-image-extra-small" src="{{ post.avatar }}">
|
||||
{{ avatar(full_name=post.full_name, image=post.avatar, size='avatar-medium') }}
|
||||
<div class="text-muted">
|
||||
<a href="/blog?blogger={{ post.blogger }}">{{ post.full_name }}</a>
|
||||
<div class="small">
|
||||
|
|
|
|||
|
|
@ -2,41 +2,42 @@
|
|||
|
||||
{% block title %}{{ _(title) }}{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<h3>{{ _(title) }}</h3>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{% if has_header and login_required %}
|
||||
{% include "templates/includes/breadcrumbs.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block header_actions %}
|
||||
{% if is_list %}
|
||||
<div class="list-view-actions"></div>
|
||||
{% else %}
|
||||
<div class="web-form-actions"></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block breadcrumbs %}{% endblock %}
|
||||
|
||||
{% macro container_attributes() %}
|
||||
data-web-form="{{ name }}" data-web-form-doctype="{{ doc_type }}" data-login-required="{{ frappe.utils.cint(login_required and frappe.session.user=='Guest') }}" data-is-list="{{ frappe.utils.cint(is_list) }}" data-allow-delete="{{ allow_delete }}"
|
||||
{% endmacro %}
|
||||
|
||||
{% block page_content %}
|
||||
<div>
|
||||
|
||||
{% if has_header and login_required and allow_multiple %}
|
||||
<!-- breadcrumb -->
|
||||
{% include "templates/includes/breadcrumbs.html" %}
|
||||
{% else %}
|
||||
<div style="height: 3rem"></div>
|
||||
{% endif %}
|
||||
|
||||
<!-- main card -->
|
||||
<div class="frappe-card {{ frappe.utils.cint(is_list) and 'list-card' or '' }}">
|
||||
{% if is_list %}
|
||||
{# web form list #}
|
||||
<!-- list -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<h3>{{ _(title) }}</h3>
|
||||
<div class="list-view-actions"></div>
|
||||
</div>
|
||||
|
||||
<div class="web-form-wrapper" {{ container_attributes() }}></div>
|
||||
<div id="list-filters" class="row mt-4"></div>
|
||||
<div id="datatable" class="pt-4 overflow-auto"></div>
|
||||
<div id="list-table" class="list-table pt-4 overflow-auto"></div>
|
||||
<div class="list-view-footer text-right"></div>
|
||||
{% else %}
|
||||
{# web form #}
|
||||
<!-- web form -->
|
||||
<div class="d-flex justify-content-between web-form-head">
|
||||
<h3>{{ _(title) }}</h3>
|
||||
<div class="web-form-actions"></div>
|
||||
</div>
|
||||
<div role="form">
|
||||
<div id="introduction" class="text-muted"></div>
|
||||
<hr>
|
||||
<div id="introduction" class="text-muted hidden"></div>
|
||||
<div class="web-form-wrapper" {{ container_attributes() }}></div>
|
||||
<div class="web-form-footer text-right"></div>
|
||||
</div>
|
||||
|
|
@ -61,15 +62,16 @@ data-web-form="{{ name }}" data-web-form-doctype="{{ doc_type }}" data-login-req
|
|||
</div>
|
||||
{% endif %} {# attachments #}
|
||||
|
||||
{% if allow_comments and not frappe.form_dict.new and not is_list -%}
|
||||
<div class="comments mt-6">
|
||||
<h3>{{ _("Comments") }}</h3>
|
||||
{% include 'templates/includes/comments/comments.html' %}
|
||||
</div>
|
||||
{%- endif %} {# comments #}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if allow_comments and not frappe.form_dict.new and not is_list -%}
|
||||
<!-- comments -->
|
||||
<div class="comments" style="margin-top: 3rem;">
|
||||
{% include 'templates/includes/comments/comments.html' %}
|
||||
</div>
|
||||
{%- endif %} {# comments #}
|
||||
|
||||
{% endblock page_content %}
|
||||
|
||||
{% block script %}
|
||||
|
|
@ -132,6 +134,9 @@ frappe.init_client_script = () => {
|
|||
{% endif %}
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
{% if style is defined %}
|
||||
{{ style }}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -183,7 +183,8 @@
|
|||
},
|
||||
{
|
||||
"fieldname": "introduction_text",
|
||||
"fieldtype": "Text Editor",
|
||||
"fieldtype": "Small Text",
|
||||
"ignore_xss_filter": 1,
|
||||
"label": "Introduction"
|
||||
},
|
||||
{
|
||||
|
|
@ -234,7 +235,7 @@
|
|||
"label": "Success Message"
|
||||
},
|
||||
{
|
||||
"description": "Go to this URL after completing the form (only for Guest users)",
|
||||
"description": "Go to this URL after completing the form",
|
||||
"fieldname": "success_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Success URL"
|
||||
|
|
@ -368,7 +369,7 @@
|
|||
"icon": "icon-edit",
|
||||
"is_published_field": "published",
|
||||
"links": [],
|
||||
"modified": "2021-11-15 14:12:44.624573",
|
||||
"modified": "2022-03-23 15:44:41.385001",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Web Form",
|
||||
|
|
@ -386,6 +387,7 @@
|
|||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ frappe.ui.form.on('Web Page', {
|
|||
frm.set_query('web_template', 'page_blocks', function() {
|
||||
return {
|
||||
filters: {
|
||||
"type": 'Section'
|
||||
"type": ['in', ['Section', 'Component']]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,8 +13,12 @@
|
|||
"add_container",
|
||||
"add_top_padding",
|
||||
"add_bottom_padding",
|
||||
"add_border_at_top",
|
||||
"add_border_at_bottom",
|
||||
"add_shade",
|
||||
"hide_block"
|
||||
"hide_block",
|
||||
"add_background_image",
|
||||
"background_image"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -68,18 +72,42 @@
|
|||
"default": "1",
|
||||
"fieldname": "add_top_padding",
|
||||
"fieldtype": "Check",
|
||||
"label": "Add Space on Top"
|
||||
"label": "Add Space at Top"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "add_bottom_padding",
|
||||
"fieldtype": "Check",
|
||||
"label": "Add Space on Bottom"
|
||||
"label": "Add Space at Bottom"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "add_border_at_top",
|
||||
"fieldtype": "Check",
|
||||
"label": "Add Border at Top"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "add_border_at_bottom",
|
||||
"fieldtype": "Check",
|
||||
"label": "Add Border at Bottom"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "add_background_image",
|
||||
"fieldtype": "Check",
|
||||
"label": "Add Background Image"
|
||||
},
|
||||
{
|
||||
"depends_on": "add_background_image",
|
||||
"fieldname": "background_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Background Image"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-11 15:21:54.247652",
|
||||
"modified": "2022-03-21 14:23:32.665108",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Web Page Block",
|
||||
|
|
@ -88,5 +116,6 @@
|
|||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -111,7 +111,7 @@ def get_website_settings(context=None):
|
|||
'footer_items': get_items('footer_items'),
|
||||
"post_login": [
|
||||
{"label": _("My Account"), "url": "/me"},
|
||||
{"label": _("Logout"), "url": "/?cmd=web_logout"}
|
||||
{"label": _("Log out"), "url": "/?cmd=web_logout"}
|
||||
]
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -423,6 +423,18 @@ $.extend(frappe, {
|
|||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
setup_videos: () => {
|
||||
// converts video images into youtube embeds (via Page Builder)
|
||||
$('.section-video-wrapper').on('click', (e) => {
|
||||
let $video = $(e.currentTarget);
|
||||
let id = $video.data('youtubeId');
|
||||
console.log(id);
|
||||
$video.find(".video-thumbnail").hide();
|
||||
$video.append(`
|
||||
<iframe allowfullscreen="" class="section-video" f;rameborder="0" src="//youtube.com/embed/${id}?autoplay=1"></iframe>
|
||||
`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -647,5 +659,6 @@ $(document).on("page-change", function() {
|
|||
|
||||
frappe.ready(function() {
|
||||
frappe.show_language_picker();
|
||||
frappe.setup_videos();
|
||||
frappe.socketio.init(window.socketio_port);
|
||||
});
|
||||
|
|
|
|||
0
frappe/website/web_template/cover_image/__init__.py
Normal file
0
frappe/website/web_template/cover_image/__init__.py
Normal file
5
frappe/website/web_template/cover_image/cover_image.html
Normal file
5
frappe/website/web_template/cover_image/cover_image.html
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{{ frappe.render_template('templates/includes/image_with_blur.html', {
|
||||
"src": url,
|
||||
"alt": description,
|
||||
"class": "full-width-image"
|
||||
}) }}
|
||||
34
frappe/website/web_template/cover_image/cover_image.json
Normal file
34
frappe/website/web_template/cover_image/cover_image.json
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"__islocal": true,
|
||||
"__unsaved": 1,
|
||||
"creation": "2022-03-15 14:17:49.482939",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Template",
|
||||
"fields": [
|
||||
{
|
||||
"__islocal": 1,
|
||||
"__unsaved": 1,
|
||||
"fieldname": "url",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Image",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"__islocal": 1,
|
||||
"__unsaved": 1,
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Data",
|
||||
"label": "Description",
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2022-03-15 14:17:49.482939",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Cover Image",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
||||
"template": "",
|
||||
"type": "Component"
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"__unsaved": 1,
|
||||
"creation": "2020-04-17 16:03:35.676241",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Template",
|
||||
|
|
@ -17,8 +18,9 @@
|
|||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2020-09-11 15:52:40.656939",
|
||||
"modified": "2022-03-15 14:17:17.563982",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Full Width Image",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@
|
|||
{%- if primary_action or secondary_action -%}
|
||||
<div class="hero-buttons">
|
||||
{%- if primary_action -%}
|
||||
<a class="btn btn-lg btn-primary" href="{{ primary_action }}">
|
||||
<a class="btn btn-lg btn-dark" href="{{ primary_action }}">
|
||||
{{ primary_action_label }}
|
||||
</a>
|
||||
{%- endif -%}
|
||||
{%- if secondary_action -%}
|
||||
<a class="btn btn-lg btn-primary-light" href="{{ secondary_action }}">
|
||||
<a class="btn btn-lg btn-light ml-3" href="{{ secondary_action }}">
|
||||
{{ secondary_action_label }}
|
||||
</a>
|
||||
{%- endif -%}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"__unsaved": 1,
|
||||
"creation": "2020-04-19 15:26:23.140620",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Template",
|
||||
|
|
@ -49,7 +50,7 @@
|
|||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2020-10-26 17:39:56.959008",
|
||||
"modified": "2022-03-21 14:30:14.405261",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Hero",
|
||||
|
|
|
|||
|
|
@ -4,8 +4,12 @@
|
|||
{%- if subtitle -%}
|
||||
<p class="subtitle">{{ subtitle }}</p>
|
||||
{%- endif -%}
|
||||
<div class="mt-6">
|
||||
<a href="{{ cta_url }}" class="btn btn-lg btn-primary">{{ cta_label }}</a>
|
||||
<div class="mt-3">
|
||||
<h4 class="action">
|
||||
<a href="{{ cta_url }}" class="no-decoration">{{ cta_label }}
|
||||
<svg class="icon icon-md"><use xlink:href="#icon-right"></use></svg>
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
{%- if cta_description -%}
|
||||
<div class="description">
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
<div class="section-cta-container">
|
||||
<div class="section-small-cta">
|
||||
<div>
|
||||
<h2 class="title">{{ title or '' }}</h2>
|
||||
<h3 class="section-title">{{ title or '' }}</h3>
|
||||
{%- if subtitle -%}
|
||||
<p class="subtitle">{{ subtitle }}</p>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
<div>
|
||||
{%- if cta_label and cta_url -%}
|
||||
<a href="{{ cta_url }}" class="btn btn-lg btn-primary">{{ cta_label }}</a>
|
||||
<h4 class="action">
|
||||
<a href="{{ cta_url }}" class="no-decoration">{{ cta_label }}
|
||||
<svg class="icon icon-md"><use xlink:href="#icon-right"></use></svg>
|
||||
</a>
|
||||
</h4>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
{% from "frappe/templates/includes/avatar_macro.html" import avatar %}
|
||||
|
||||
<div class="section-with-features">
|
||||
{%- if title -%}
|
||||
<h2 class="section-title">{{ title }}</h2>
|
||||
{%- endif -%}
|
||||
{%- if subtitle -%}
|
||||
<p class="section-description">{{ subtitle }}</p>
|
||||
{%- endif -%}
|
||||
|
||||
<div class="section-features" data-columns="{{ columns or 3 }}">
|
||||
{%- for testimonial in testimonials -%}
|
||||
<div class="section-feature">
|
||||
<div>
|
||||
{%- if testimonial.content -%}
|
||||
<p class="feature-content">{{ testimonial.content }}</p>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
<div class="testimonial-author">
|
||||
{{ avatar(full_name=testimonial.full_name, image=testimonial.image, size='avatar-medium') }}
|
||||
<p>
|
||||
{{ testimonial.full_name }}
|
||||
{%- if testimonial.designation -%}
|
||||
<br>{{ testimonial.designation }}
|
||||
{%- endif -%}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"__unsaved": 1,
|
||||
"creation": "2022-03-21 15:28:13.141783",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Template",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "subtitle",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subtitle",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"default": "3",
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Select",
|
||||
"label": "Columns",
|
||||
"options": "2\n3\n4",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "testimonials",
|
||||
"fieldtype": "Table Break",
|
||||
"label": "Testimonials",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "content",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Content",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "full_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Full Name",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "designation",
|
||||
"fieldtype": "Data",
|
||||
"label": "Designation",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Image",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "url",
|
||||
"fieldtype": "Data",
|
||||
"label": "URL",
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2022-03-21 15:39:39.044104",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Section with Testimonials",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
||||
"template": "",
|
||||
"type": "Section"
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<div class="section-with-features">
|
||||
{%- if title -%}
|
||||
<h2 class="section-title">{{ title }}</h2>
|
||||
{%- endif -%}
|
||||
{%- if subtitle -%}
|
||||
<p class="section-description">{{ subtitle }}</p>
|
||||
{%- endif -%}
|
||||
|
||||
<div class="section-features" data-columns="{{ columns or 3 }}">
|
||||
{%- for video in videos -%}
|
||||
<div class="section-feature">
|
||||
<div class="section-video-wrapper" data-youtube-id="{{ video.youtube_id }}">
|
||||
<img class="video-thumbnail" src="https://i.ytimg.com/vi/{{ video.youtube_id }}/sddefault.jpg">
|
||||
</div>
|
||||
{%- if video.title -%}
|
||||
<h3 class="feature-title">{{ video.title }}</h3>
|
||||
{%- endif -%}
|
||||
{%- if video.content -%}
|
||||
<p class="feature-content">{{ video.content }}</p>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"__unsaved": 1,
|
||||
"creation": "2022-03-21 15:59:18.432776",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Template",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "subtitle",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subtitle",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"default": "3",
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Select",
|
||||
"label": "Columns",
|
||||
"options": "2\n3\n4",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "videos",
|
||||
"fieldtype": "Table Break",
|
||||
"label": "Videos",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "youtube_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "YouTube Video ID",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "content",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Content",
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2022-03-21 16:03:46.339279",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Section with Videos",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
||||
"template": "",
|
||||
"type": "Section"
|
||||
}
|
||||
|
|
@ -3,10 +3,9 @@
|
|||
{% block title %}
|
||||
{{ _("My Account") }}
|
||||
{% endblock %}
|
||||
{% block header %}
|
||||
<h3 class="my-account-header">{{_("My Account") }}</h3>
|
||||
{% endblock %}
|
||||
{% block page_content %}
|
||||
<div class="my-account-container">
|
||||
<h3 class="my-account-header">{{_("My Account") }}</h3>
|
||||
<div class="row account-info d-flex flex-column">
|
||||
<div class="col d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
|
|
@ -79,16 +78,5 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row d-block d-sm-none">
|
||||
<div class="col-12 side-list">
|
||||
<ul class="list-group">
|
||||
{% for item in sidebar_items -%}
|
||||
<a class="list-group-item" href="{{ item.route }}"
|
||||
{% if item.target %}target="{{ item.target }}"{% endif %}>
|
||||
{{ _(item.title or item.label) }}
|
||||
</a>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -12,4 +12,3 @@ def get_context(context):
|
|||
frappe.throw(_("You need to be logged in to access this page"), frappe.PermissionError)
|
||||
|
||||
context.current_user = frappe.get_doc("User", frappe.session.user)
|
||||
context.show_sidebar=True
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
{% extends "templates/web.html" %}
|
||||
|
||||
{% block title %} {{ _("Third Party Apps") }} {% endblock %}
|
||||
{% block header %}
|
||||
<h3 class="my-account-header">{{ _("Third Party Apps") }}</h3>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_sidebar %}
|
||||
{% include "templates/includes/web_sidebar.html" %}
|
||||
|
|
@ -13,25 +10,24 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<div class='padding'></div>
|
||||
|
||||
<h3 class="my-account-header">{{ _("Third Party Apps") }}</h3>
|
||||
<div class="third-party-wrapper">
|
||||
{% if app %}
|
||||
<h4>{{ app.app_name }}</h4>
|
||||
<div class="web-list-item">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="well">
|
||||
<div class="text-muted">{{ _("This will log out {0} from all other devices").format(app.app_name) }}</div>
|
||||
<div class="padding"></div>
|
||||
<div class="text-right">
|
||||
<button class="btn btn-default" onclick="location.href = '/third_party_apps';">Cancel</button>
|
||||
<button class="btn btn-danger btn-delete-app" data-client_id="{{ app.client_id }}">Revoke</button>
|
||||
</div>
|
||||
<div class="web-list-item">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="well">
|
||||
<div class="text-muted">{{ _("This will log out {0} from all other devices").format(app.app_name) }}</div>
|
||||
<div class="padding"></div>
|
||||
<div class="text-right">
|
||||
<button class="btn btn-default" onclick="location.href = '/third_party_apps';">Cancel</button>
|
||||
<button class="btn btn-danger btn-delete-app" data-client_id="{{ app.client_id }}">Revoke</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif apps|length > 0 %}
|
||||
<h4>{{ _("Active Sessions") }}</h4>
|
||||
{% for app in apps %}
|
||||
|
|
@ -62,9 +58,37 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="padding"></div>
|
||||
</div>
|
||||
<script>
|
||||
{% include "templates/includes/integrations/third_party_apps.js" %}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.my-account-header, .third-party-wrapper {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.my-account-header {
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.third-party-wrapper {
|
||||
background-color: var(--fg-color);
|
||||
border-radius: var(--border-radius-md);
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
.empty-apps-state {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
padding-top: 6rem;
|
||||
padding-bottom: 6rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ def get_context(context):
|
|||
context.app = app
|
||||
|
||||
context.apps = client_apps
|
||||
context.show_sidebar = True
|
||||
|
||||
def get_first_login(client):
|
||||
login_date = frappe.get_all("OAuth Bearer Token",
|
||||
|
|
@ -49,4 +48,4 @@ def get_first_login(client):
|
|||
def delete_client(client_id):
|
||||
active_client_id_tokens = frappe.get_all("OAuth Bearer Token", filters=[["user", "=", frappe.session.user], ["client","=", client_id]])
|
||||
for token in active_client_id_tokens:
|
||||
frappe.delete_doc("OAuth Bearer Token", token.get("name"), ignore_permissions=True)
|
||||
frappe.delete_doc("OAuth Bearer Token", token.get("name"), ignore_permissions=True)
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@
|
|||
<form id="reset-password">
|
||||
<div class="form-group">
|
||||
<input id="old_password" type="password"
|
||||
class="form-control" placeholder="{{ _('Old Password') }}">
|
||||
class="form-control mb-4" placeholder="{{ _('Old Password') }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input id="new_password" type="password"
|
||||
class="form-control" placeholder="{{ _('New Password') }}">
|
||||
class="form-control mb-4" placeholder="{{ _('New Password') }}">
|
||||
<span class="password-strength-indicator indicator"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
|
@ -216,6 +216,10 @@ frappe.ready(function() {
|
|||
|
||||
{% block style %}
|
||||
<style>
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.password-strength-indicator {
|
||||
float: right;
|
||||
padding: 15px;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue