Merge branch 'develop' into discussions-component-redesign

This commit is contained in:
Jannat Patel 2022-03-28 10:21:19 +05:30 committed by GitHub
commit 653bb909d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 1336 additions and 658 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = []

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -111,8 +111,8 @@
}
.avatar-large {
width: 72px;
height: 72px;
width: 64px;
height: 64px;
.standard-image {
font-size: var(--text-2xl);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,5 @@
{{ frappe.render_template('templates/includes/image_with_blur.html', {
"src": url,
"alt": description,
"class": "full-width-image"
}) }}

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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