@@ -5,6 +5,7 @@ pull_request_rules: | |||
- and: | |||
- author!=surajshetty3416 | |||
- author!=gavindsouza | |||
- author!=deepeshgarg007 | |||
- or: | |||
- base=version-13 | |||
- base=version-12 | |||
@@ -0,0 +1,128 @@ | |||
context('Dynamic Link', () => { | |||
before(() => { | |||
cy.login(); | |||
cy.visit('/app/doctype'); | |||
return cy.window().its('frappe').then(frappe => { | |||
return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', { | |||
name: 'Test Dynamic Link', | |||
fields: [ | |||
{ | |||
"label": "Document Type", | |||
"fieldname": "doc_type", | |||
"fieldtype": "Link", | |||
"options": "DocType", | |||
"in_list_view": 1, | |||
"in_standard_filter": 1, | |||
}, | |||
{ | |||
"label": "Document ID", | |||
"fieldname": "doc_id", | |||
"fieldtype": "Dynamic Link", | |||
"options": "doc_type", | |||
"in_list_view": 1, | |||
"in_standard_filter": 1, | |||
}, | |||
] | |||
}); | |||
}); | |||
}); | |||
function get_dialog_with_dynamic_link() { | |||
return cy.dialog({ | |||
title: 'Dynamic Link', | |||
fields: [{ | |||
"label": "Document Type", | |||
"fieldname": "doc_type", | |||
"fieldtype": "Link", | |||
"options": "DocType", | |||
"in_list_view": 1, | |||
}, | |||
{ | |||
"label": "Document ID", | |||
"fieldname": "doc_id", | |||
"fieldtype": "Dynamic Link", | |||
"options": "doc_type", | |||
"in_list_view": 1, | |||
}] | |||
}); | |||
} | |||
function get_dialog_with_dynamic_link_option() { | |||
return cy.dialog({ | |||
title: 'Dynamic Link', | |||
fields: [{ | |||
"label": "Document Type", | |||
"fieldname": "doc_type", | |||
"fieldtype": "Link", | |||
"options": "DocType", | |||
"in_list_view": 1, | |||
}, | |||
{ | |||
"label": "Document ID", | |||
"fieldname": "doc_id", | |||
"fieldtype": "Dynamic Link", | |||
"get_options": () => { | |||
return "User"; | |||
}, | |||
"in_list_view": 1, | |||
}] | |||
}); | |||
} | |||
it('Creating a dynamic link by passing option as function and verifying it in a dialog', () => { | |||
get_dialog_with_dynamic_link_option().as('dialog'); | |||
cy.get_field('doc_type').clear(); | |||
cy.fill_field('doc_type', 'User', 'Link'); | |||
cy.get_field('doc_id').click(); | |||
//Checking if the listbox have length greater than 0 | |||
cy.get('[data-fieldname="doc_id"]').find('.awesomplete').find("li").its('length').should('be.gte', 0); | |||
cy.get('.btn-modal-close').click({force: true}); | |||
}); | |||
it('Creating a dynamic link and verifying it in a dialog', () => { | |||
get_dialog_with_dynamic_link().as('dialog'); | |||
cy.get_field('doc_type').clear(); | |||
cy.fill_field('doc_type', 'User', 'Link'); | |||
cy.get_field('doc_id').click(); | |||
//Checking if the listbox have length greater than 0 | |||
cy.get('[data-fieldname="doc_id"]').find('.awesomplete').find("li").its('length').should('be.gte', 0); | |||
cy.get('.btn-modal-close').click({force: true, multiple: true}); | |||
}); | |||
it('Creating a dynamic link and verifying it', () => { | |||
cy.visit('/app/test-dynamic-link'); | |||
//Clicking on the Document ID field | |||
cy.get_field('doc_type').clear(); | |||
//Entering User in the Doctype field | |||
cy.fill_field('doc_type', 'User', 'Link', {delay: 500}); | |||
cy.get_field('doc_id').click(); | |||
//Checking if the listbox have length greater than 0 | |||
cy.get('[data-fieldname="doc_id"]').find('.awesomplete').find("li").its('length').should('be.gte', 0); | |||
//Opening a new form for dynamic link doctype | |||
cy.new_form('Test Dynamic Link'); | |||
cy.get_field('doc_type').clear(); | |||
//Entering User in the Doctype field | |||
cy.fill_field('doc_type', 'User', 'Link', {delay: 500}); | |||
cy.get_field('doc_id').click(); | |||
//Checking if the listbox have length greater than 0 | |||
cy.get('[data-fieldname="doc_id"]').find('.awesomplete').find("li").its('length').should('be.gte', 0); | |||
cy.get_field('doc_type').clear(); | |||
//Entering System Settings in the Doctype field | |||
cy.fill_field('doc_type', 'System Settings', 'Link', {delay: 500}); | |||
cy.get_field('doc_id').click(); | |||
//Checking if the system throws error | |||
cy.get('.modal-title').should('have.text', 'Error'); | |||
cy.get('.msgprint').should('have.text', 'System Settings is not a valid DocType for Dynamic Link'); | |||
}); | |||
}); |
@@ -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'); | |||
}); | |||
}); | |||
}); |
@@ -17,6 +17,9 @@ | |||
import './commands'; | |||
import '@cypress/code-coverage/support'; | |||
Cypress.on('uncaught:exception', (err, runnable) => { | |||
return false; | |||
}); | |||
// Alternatively you can use CommonJS syntax: | |||
// require('./commands') | |||
@@ -1,4 +1,4 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
""" | |||
Frappe - Low Code Open Source Framework in Python and JS | |||
@@ -20,10 +20,10 @@ if _dev_server: | |||
warnings.simplefilter('always', DeprecationWarning) | |||
warnings.simplefilter('always', PendingDeprecationWarning) | |||
from werkzeug.local import Local, release_local | |||
import sys, importlib, inspect, json | |||
import typing | |||
import click | |||
from werkzeug.local import Local, release_local | |||
from typing import TYPE_CHECKING, Dict, List, Union | |||
# Local application imports | |||
from .exceptions import * | |||
@@ -143,15 +143,14 @@ lang = local("lang") | |||
# This if block is never executed when running the code. It is only used for | |||
# telling static code analyzer where to find dynamically defined attributes. | |||
if typing.TYPE_CHECKING: | |||
from frappe.utils.redis_wrapper import RedisWrapper | |||
if TYPE_CHECKING: | |||
from frappe.database.mariadb.database import MariaDBDatabase | |||
from frappe.database.postgres.database import PostgresDatabase | |||
from frappe.query_builder.builder import MariaDB, Postgres | |||
from frappe.utils.redis_wrapper import RedisWrapper | |||
db: typing.Union[MariaDBDatabase, PostgresDatabase] | |||
qb: typing.Union[MariaDB, Postgres] | |||
db: Union[MariaDBDatabase, PostgresDatabase] | |||
qb: Union[MariaDB, Postgres] | |||
# end: static analysis hack | |||
@@ -897,7 +896,12 @@ def clear_document_cache(doctype, name): | |||
cache().hdel('document_cache', key) | |||
def get_cached_value(doctype, name, fieldname, as_dict=False): | |||
doc = get_cached_doc(doctype, name) | |||
try: | |||
doc = get_cached_doc(doctype, name) | |||
except DoesNotExistError: | |||
clear_last_message() | |||
return | |||
if isinstance(fieldname, str): | |||
if as_dict: | |||
throw('Cannot make dict for single fieldname') | |||
@@ -1465,7 +1469,7 @@ def get_list(doctype, *args, **kwargs): | |||
:param fields: List of fields or `*`. | |||
:param filters: List of filters (see example). | |||
:param order_by: Order By e.g. `modified desc`. | |||
:param limit_page_start: Start results at record #. Default 0. | |||
:param limit_start: Start results at record #. Default 0. | |||
:param limit_page_length: No of records in the page. Default 20. | |||
Example usage: | |||
@@ -1523,12 +1527,16 @@ def get_value(*args, **kwargs): | |||
""" | |||
return db.get_value(*args, **kwargs) | |||
def as_json(obj, indent=1): | |||
def as_json(obj: Union[Dict, List], indent=1) -> str: | |||
from frappe.utils.response import json_handler | |||
try: | |||
return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler, separators=(',', ': ')) | |||
except TypeError: | |||
return json.dumps(obj, indent=indent, default=json_handler, separators=(',', ': ')) | |||
# this would break in case the keys are not all os "str" type - as defined in the JSON | |||
# adding this to ensure keys are sorted (expected behaviour) | |||
sorted_obj = dict(sorted(obj.items(), key=lambda kv: str(kv[0]))) | |||
return json.dumps(sorted_obj, indent=indent, default=json_handler, separators=(',', ': ')) | |||
def are_emails_muted(): | |||
from frappe.utils import cint | |||
@@ -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" | |||
@@ -153,7 +153,7 @@ | |||
"fieldname": "communication_type", | |||
"fieldtype": "Select", | |||
"label": "Communication Type", | |||
"options": "Communication\nComment\nChat\nBot\nNotification\nFeedback\nAutomated Message", | |||
"options": "Communication\nComment\nChat\nNotification\nFeedback\nAutomated Message", | |||
"read_only": 1, | |||
"reqd": 1 | |||
}, | |||
@@ -164,7 +164,7 @@ | |||
"in_list_view": 1, | |||
"in_standard_filter": 1, | |||
"label": "Comment Type", | |||
"options": "\nComment\nLike\nInfo\nLabel\nWorkflow\nCreated\nSubmitted\nCancelled\nUpdated\nDeleted\nAssigned\nAssignment Completed\nAttachment\nAttachment Removed\nShared\nUnshared\nBot\nRelinked", | |||
"options": "\nComment\nLike\nInfo\nLabel\nWorkflow\nCreated\nSubmitted\nCancelled\nUpdated\nDeleted\nAssigned\nAssignment Completed\nAttachment\nAttachment Removed\nShared\nUnshared\nRelinked", | |||
"read_only": 1 | |||
}, | |||
{ | |||
@@ -395,7 +395,7 @@ | |||
"icon": "fa fa-comment", | |||
"idx": 1, | |||
"links": [], | |||
"modified": "2021-11-30 09:03:25.728637", | |||
"modified": "2022-03-30 11:24:25.728637", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "Communication", | |||
@@ -10,7 +10,6 @@ from frappe.utils import validate_email_address, strip_html, cstr, time_diff_in_ | |||
from frappe.core.doctype.communication.email import validate_email | |||
from frappe.core.doctype.communication.mixins import CommunicationEmailMixin | |||
from frappe.core.utils import get_parent_doc | |||
from frappe.utils.bot import BotReply | |||
from frappe.utils import parse_addr, split_emails | |||
from frappe.core.doctype.comment.comment import update_comment_in_doc | |||
from email.utils import getaddresses | |||
@@ -105,7 +104,7 @@ class Communication(Document, CommunicationEmailMixin): | |||
if self.communication_type == "Communication": | |||
self.notify_change('add') | |||
elif self.communication_type in ("Chat", "Notification", "Bot"): | |||
elif self.communication_type in ("Chat", "Notification"): | |||
if self.reference_name == frappe.session.user: | |||
message = self.as_dict() | |||
message['broadcast'] = True | |||
@@ -160,7 +159,6 @@ class Communication(Document, CommunicationEmailMixin): | |||
if self.comment_type != 'Updated': | |||
update_parent_document_on_communication(self) | |||
self.bot_reply() | |||
def on_trash(self): | |||
if self.communication_type == "Communication": | |||
@@ -278,20 +276,6 @@ class Communication(Document, CommunicationEmailMixin): | |||
if not self.sender_full_name: | |||
self.sender_full_name = sender_email | |||
def bot_reply(self): | |||
if self.comment_type == 'Bot' and self.communication_type == 'Chat': | |||
reply = BotReply().get_reply(self.content) | |||
if reply: | |||
frappe.get_doc({ | |||
"doctype": "Communication", | |||
"comment_type": "Bot", | |||
"communication_type": "Bot", | |||
"content": cstr(reply), | |||
"reference_doctype": self.reference_doctype, | |||
"reference_name": self.reference_name | |||
}).insert() | |||
frappe.local.flags.commit = True | |||
def set_delivery_status(self, commit=False): | |||
'''Look into the status of Email Queue linked to this Communication and set the Delivery Status of this Communication''' | |||
delivery_status = None | |||
@@ -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 | |||
} | |||
] |
@@ -119,8 +119,8 @@ class Database(object): | |||
if not run: | |||
return query | |||
# remove \n \t from start and end of query | |||
query = re.sub(r'^\s*|\s*$', '', query) | |||
# remove whitespace / indentation from start and end of query | |||
query = query.strip() | |||
if re.search(r'ifnull\(', query, flags=re.IGNORECASE): | |||
# replaces ifnull in query with coalesce | |||
@@ -357,6 +357,7 @@ class Database(object): | |||
order_by="KEEP_DEFAULT_ORDERING", | |||
cache=False, | |||
for_update=False, | |||
*, | |||
run=True, | |||
pluck=False, | |||
distinct=False, | |||
@@ -386,17 +387,27 @@ class Database(object): | |||
frappe.db.get_value("System Settings", None, "date_format") | |||
""" | |||
ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug, | |||
result = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug, | |||
order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct, limit=1) | |||
if not run: | |||
return ret | |||
return result | |||
if not result: | |||
return None | |||
row = result[0] | |||
if len(row) > 1 or as_dict: | |||
return row | |||
else: | |||
# single field is requested, send it without wrapping in containers | |||
return row[0] | |||
return ((len(ret[0]) > 1 or as_dict) and ret[0] or ret[0][0]) if ret else None | |||
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False, | |||
debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False, | |||
run=True, pluck=False, distinct=False, limit=None): | |||
*, run=True, pluck=False, distinct=False, limit=None): | |||
"""Returns multiple document properties. | |||
:param doctype: DocType name. | |||
@@ -487,6 +498,7 @@ class Database(object): | |||
as_dict=False, | |||
debug=False, | |||
update=None, | |||
*, | |||
run=True, | |||
pluck=False, | |||
distinct=False, | |||
@@ -621,7 +633,8 @@ class Database(object): | |||
filters, | |||
doctype, | |||
as_dict, | |||
debug, | |||
*, | |||
debug=False, | |||
order_by=None, | |||
update=None, | |||
for_update=False, | |||
@@ -661,7 +674,7 @@ class Database(object): | |||
) | |||
return r | |||
def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False, limit=None): | |||
def _get_value_for_many_names(self, doctype, names, field, order_by, *, debug=False, run=True, pluck=False, distinct=False, limit=None): | |||
names = list(filter(None, names)) | |||
if names: | |||
return self.get_all( | |||
@@ -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=["*"]) | |||
sorted_users = sorted(users, key=lambda k: k['user']) | |||
grouped_by_user = {} | |||
for k, v in groupby(sorted_users, key=lambda k: k['user']): | |||
grouped_by_user[k] = list(v) | |||
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) | |||
}) | |||
if message and frappe.db.get_value("User", user, "document_follow_notify", ignore=True): | |||
send_email_alert(user, valid_document_follows, message) | |||
user_list = get_user_list(frequency) | |||
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() | |||
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") | |||
def get_message_for_user(frequency, user): | |||
message = [] | |||
latest_document_follows = get_document_followed_by_user(user) | |||
valid_document_follows = [] | |||
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() | |||
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() | |||
self.assertIn(event_doc.doctype, email_queue_entry_doc.message) | |||
self.assertIn(event_doc.name, email_queue_entry_doc.message) | |||
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 |
@@ -282,14 +282,6 @@ sounds = [ | |||
# {"name": "chime", "src": "/assets/frappe/sounds/chime.mp3"}, | |||
] | |||
bot_parsers = [ | |||
'frappe.utils.bot.ShowNotificationBot', | |||
'frappe.utils.bot.GetOpenListBot', | |||
'frappe.utils.bot.ListBot', | |||
'frappe.utils.bot.FindBot', | |||
'frappe.utils.bot.CountBot' | |||
] | |||
setup_wizard_exception = [ | |||
"frappe.desk.page.setup_wizard.setup_wizard.email_setup_wizard_exception", | |||
"frappe.desk.page.setup_wizard.setup_wizard.log_setup_wizard_exception" | |||
@@ -963,10 +963,13 @@ class BaseDocument(object): | |||
from frappe.model.meta import get_default_df | |||
df = get_default_df(fieldname) | |||
if df.fieldtype == "Currency" and not currency: | |||
currency = self.get(df.get("options")) | |||
if not frappe.db.exists('Currency', currency, cache=True): | |||
currency = None | |||
if ( | |||
df.fieldtype == "Currency" | |||
and not currency | |||
and (currency_field := df.get("options")) | |||
and (currency_value := self.get(currency_field)) | |||
): | |||
currency = frappe.db.get_value('Currency', currency_value, cache=True) | |||
val = self.get(fieldname) | |||
@@ -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 | |||
@@ -37,7 +37,7 @@ frappe.ui.form.PrintView = class { | |||
this.print_wrapper = this.page.main.empty().html( | |||
`<div class="print-preview-wrapper"><div class="print-preview"> | |||
${frappe.render_template('print_skeleton_loading')} | |||
<iframe class="print-format-container" width="100%" height="0" frameBorder="0" scrolling="no""> | |||
<iframe class="print-format-container" width="100%" height="0" frameBorder="0" scrolling="no"> | |||
</iframe> | |||
</div> | |||
<div class="page-break-message text-muted text-center text-medium margin-top"></div> | |||
@@ -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) { | |||
@@ -124,7 +124,7 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { | |||
toggle_theme(theme) { | |||
this.current_theme = theme.toLowerCase(); | |||
document.documentElement.setAttribute("data-theme-mode", this.current_theme); | |||
frappe.show_alert("Theme Changed", 3); | |||
frappe.show_alert(__("Theme Changed"), 3); | |||
frappe.xcall("frappe.core.doctype.user.user.switch_theme", { | |||
theme: toTitle(theme) | |||
@@ -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); | |||
@@ -355,7 +355,7 @@ body[data-route^="Module"] .main-menu { | |||
display: none; | |||
} | |||
input { | |||
input:not([data-fieldtype='Check']) { | |||
background: var(--control-bg-on-gray); | |||
} | |||
@@ -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(xl) { | |||
font-size: $font-size-3xl; | |||
margin-top: 3.5rem; | |||
} | |||
@include media-breakpoint-up(md) { | |||
} | |||
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; | |||
} | |||
@@ -459,6 +479,12 @@ | |||
align-items: center; | |||
} | |||
.collapsible-item-title { | |||
font-weight: 600; | |||
color: var(--text-color); | |||
font-size: var(--text-2xl); | |||
} | |||
.collapsible-item a { | |||
text-decoration: none; | |||
} | |||
@@ -496,6 +522,7 @@ | |||
.section-description, .collapsible-items { | |||
margin-left: auto; | |||
margin-right: auto; | |||
margin-top: 3rem; | |||
} | |||
} | |||
@@ -514,12 +541,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 +555,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 +657,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 +683,14 @@ | |||
position: relative; | |||
} | |||
.feature-title { | |||
margin-top: 0; | |||
} | |||
.feature-content { | |||
line-height: 1.7; | |||
} | |||
.feature-title, .feature-content { | |||
margin-bottom: 0; | |||
} | |||
@@ -666,3 +706,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,26 +1,45 @@ | |||
@import "../common/form"; | |||
[data-doctype="Web Form"] { | |||
.page-content-wrapper { | |||
.page_content { | |||
max-width: 800px; | |||
margin: auto; | |||
.breadcrumb-container.container { | |||
@include media-breakpoint-up(sm) { | |||
padding-left: 0; | |||
.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); | |||
} | |||
} | |||
.container { | |||
max-width: 800px; | |||
#introduction { | |||
margin-bottom: 2rem; | |||
} | |||
&.my-4 { | |||
background-color: var(--fg-color); | |||
#introduction p { | |||
color: var(--text-muted); | |||
} | |||
@include media-breakpoint-up(sm) { | |||
padding: 1.8rem; | |||
border-radius: var(--border-radius-md); | |||
box-shadow: var(--card-shadow); | |||
} | |||
.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; | |||
} | |||
} | |||
} | |||
@@ -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 -%} | |||
@@ -312,6 +312,21 @@ class TestDB(unittest.TestCase): | |||
frappe.db.MAX_WRITES_PER_TRANSACTION = Database.MAX_WRITES_PER_TRANSACTION | |||
def test_transaction_write_counting(self): | |||
note = frappe.get_doc(doctype="Note", title="transaction counting").insert() | |||
writes = frappe.db.transaction_writes | |||
frappe.db.set_value("Note", note.name, "content", "abc") | |||
self.assertEqual(1, frappe.db.transaction_writes - writes) | |||
writes = frappe.db.transaction_writes | |||
frappe.db.sql(""" | |||
update `tabNote` | |||
set content = 'abc' | |||
where name = %s | |||
""", note.name) | |||
self.assertEqual(1, frappe.db.transaction_writes - writes) | |||
def test_pk_collision_ignoring(self): | |||
# note has `name` generated from title | |||
for _ in range(3): | |||
@@ -246,7 +246,7 @@ class TestDocument(unittest.TestCase): | |||
'fields': [ | |||
{'label': 'Currency', 'fieldname': 'currency', 'reqd': 1, 'fieldtype': 'Currency'}, | |||
] | |||
}).insert() | |||
}).insert(ignore_if_duplicate=True) | |||
frappe.delete_doc_if_exists("Currency", "INR", 1) | |||
@@ -262,6 +262,10 @@ class TestDocument(unittest.TestCase): | |||
}) | |||
self.assertEqual(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00') | |||
# should work even if options aren't set in df | |||
# and currency param is not passed | |||
self.assertIn("0", d.get_formatted("currency")) | |||
def test_limit_for_get(self): | |||
doc = frappe.get_doc("DocType", "DocType") | |||
# assuming DocType has more than 3 Data fields | |||
@@ -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): | |||
@@ -36,7 +36,18 @@ class TestTranslate(unittest.TestCase): | |||
def test_extract_message_from_file(self): | |||
data = frappe.translate.get_messages_from_file(translation_string_file) | |||
self.assertListEqual(data, expected_output) | |||
exp_filename = "apps/frappe/frappe/tests/translation_test_file.txt" | |||
self.assertEqual(len(data), len(expected_output), | |||
msg=f"Mismatched output:\nExpected: {expected_output}\nFound: {data}") | |||
for extracted, expected in zip(data, expected_output): | |||
ext_filename, ext_message, ext_context, ext_line = extracted | |||
exp_message, exp_context, exp_line = expected | |||
self.assertEqual(ext_filename, exp_filename) | |||
self.assertEqual(ext_message, exp_message) | |||
self.assertEqual(ext_context, exp_context) | |||
self.assertEqual(ext_line, exp_line) | |||
def test_translation_with_context(self): | |||
try: | |||
@@ -107,13 +118,16 @@ class TestTranslate(unittest.TestCase): | |||
expected_output = [ | |||
('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', 'This is some context', 2), | |||
('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', None, 4), | |||
('apps/frappe/frappe/tests/translation_test_file.txt', "You don't have any messages yet.", None, 6), | |||
('apps/frappe/frappe/tests/translation_test_file.txt', 'Submit', 'Some DocType', 8), | |||
('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', 'This is some context', 15), | |||
('apps/frappe/frappe/tests/translation_test_file.txt', 'Submit', 'Some DocType', 17), | |||
('apps/frappe/frappe/tests/translation_test_file.txt', "You don't have any messages yet.", None, 19), | |||
('apps/frappe/frappe/tests/translation_test_file.txt', "You don't have any messages yet.", None, 21) | |||
('Warning: Unable to find {0} in any table related to {1}', 'This is some context', 2), | |||
('Warning: Unable to find {0} in any table related to {1}', None, 4), | |||
("You don't have any messages yet.", None, 6), | |||
('Submit', 'Some DocType', 8), | |||
('Warning: Unable to find {0} in any table related to {1}', 'This is some context', 15), | |||
('Submit', 'Some DocType', 17), | |||
("You don't have any messages yet.", None, 19), | |||
("You don't have any messages yet.", None, 21), | |||
("Long string that needs its own line because of black formatting.", None, 24), | |||
("Long string with", "context", 28), | |||
("Long string with", "context on newline", 32), | |||
] | |||
@@ -18,4 +18,18 @@ _('Submit', context="Some DocType") | |||
_("""You don't have any messages yet.""") | |||
_('''You don't have any messages yet.''') | |||
_('''You don't have any messages yet.''') | |||
// allow newline in beginning | |||
_( | |||
"""Long string that needs its own line because of black formatting.""" | |||
).format("blah") | |||
_( | |||
"Long string with", context="context" | |||
).format("blah") | |||
_( | |||
"Long string with", | |||
context="context on newline" | |||
).format("blah") |
@@ -23,6 +23,35 @@ from frappe.utils import get_bench_path, is_html, strip, strip_html_tags | |||
from frappe.query_builder import Field, DocType | |||
from pypika.terms import PseudoColumn | |||
TRANSLATE_PATTERN = re.compile( | |||
r"_\([\s\n]*" # starts with literal `_(`, ignore following whitespace/newlines | |||
# BEGIN: message search | |||
r"([\"']{,3})" # start of message string identifier - allows: ', ", """, '''; 1st capture group | |||
r"(?P<message>((?!\1).)*)" # Keep matching until string closing identifier is met which is same as 1st capture group | |||
r"\1" # match exact string closing identifier | |||
# END: message search | |||
# BEGIN: python context search | |||
r"([\s\n]*,[\s\n]*context\s*=\s*" # capture `context=` with ignoring whitespace | |||
r"([\"'])" # start of context string identifier; 5th capture group | |||
r"(?P<py_context>((?!\5).)*)" # capture context string till closing id is found | |||
r"\5" # match context string closure | |||
r")?" # match 0 or 1 context strings | |||
# END: python context search | |||
# BEGIN: JS context search | |||
r"(\s*,\s*(.)*?\s*(,\s*" # skip message format replacements: ["format", ...] | null | [] | |||
r"([\"'])" # start of context string; 11th capture group | |||
r"(?P<js_context>((?!\11).)*)" # capture context string till closing id is found | |||
r"\11" # match context string closure | |||
r")*" | |||
r")*" # match one or more context string | |||
# END: JS context search | |||
r"[\s\n]*\)" # Closing function call ignore leading whitespace/newlines | |||
) | |||
def get_language(lang_list: List = None) -> str: | |||
"""Set `frappe.local.lang` from HTTP headers at beginning of request | |||
@@ -651,9 +680,8 @@ def extract_messages_from_code(code): | |||
frappe.clear_last_message() | |||
messages = [] | |||
pattern = r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)" | |||
for m in re.compile(pattern).finditer(code): | |||
for m in TRANSLATE_PATTERN.finditer(code): | |||
message = m.group('message') | |||
context = m.group('py_context') or m.group('js_context') | |||
pos = m.start() | |||
@@ -218,7 +218,7 @@ Route,Route, | |||
Sales Manager,Responsable des Ventes, | |||
Sales Master Manager,Directeur des Ventes, | |||
Sales User,Chargé de Ventes, | |||
Salutation,Salutations, | |||
Salutation,Civilité, | |||
Sample,Échantillon, | |||
Saturday,Samedi, | |||
Saved,Enregistré, | |||
@@ -246,7 +246,7 @@ Start Import,Démarrer l'import, | |||
State,Etat, | |||
Stopped,Arrêté, | |||
Subject,Sujet, | |||
Submit,Soumettre, | |||
Submit,Valider, | |||
Successful,Réussi, | |||
Summary,Résumé, | |||
Sunday,Dimanche, | |||
@@ -786,6 +786,7 @@ Custom Sidebar Menu,Barre Latérale Personnalisée, | |||
Custom Translations,Traductions Personnalisées, | |||
Customization,Personnalisation, | |||
Customizations Reset,Réinitialiser les Personnalisations, | |||
Reset Customizations,Réinitialiser les Personnalisations, | |||
Customizations for <b>{0}</b> exported to:<br>{1},Personnalisations pour <b>{0}</b> exportées vers: <br> {1}, | |||
Customize Form,Personnaliser le formulaire, | |||
Customize Form Field,Personnaliser un Champ de Formulaire, | |||
@@ -1508,7 +1509,7 @@ Login not allowed at this time,Connexion non autorisée pour le moment, | |||
Login to comment,Connectez-vous pour commenter, | |||
Login token required,Identifiants de Connexion Requis, | |||
Login with LDAP,Se connecter avec LDAP, | |||
Logout,Connectez - Out, | |||
Logout,Déconnecté, | |||
Long Text,Texte Long, | |||
Looks like something is wrong with this site's Paypal configuration.,Il semble qu'il y ait une erreur avec la configuration Paypal de ce site., | |||
Looks like something is wrong with this site's payment gateway configuration. No payment has been made.,On dirait que quelque chose ne va pas dans la configuration de la passerelle de paiement de ce site. Aucun paiement n'a été effectué., | |||
@@ -2164,7 +2165,7 @@ Search for '{0}',Rechercher '{0}', | |||
Search for anything,Rechercher tout, | |||
Search in a document type,Rechercher dans un type de document, | |||
Search or Create a New Chat,Rechercher ou créer un nouveau chat, | |||
Search or type a command,Rechercher ou taper une commande, | |||
Search or type a command (Ctrl + G),Rechercher ou taper une commande (Ctrl + G), | |||
Search...,Rechercher..., | |||
Searching,Recherche, | |||
Searching ...,Recherche ..., | |||
@@ -4139,7 +4140,7 @@ Document is only editable by users with role,Le document n'est modifiable qu | |||
{0}: Other permission rules may also apply,{0}: d'autres règles d'autorisation peuvent également s'appliquer, | |||
{0} Page Views,{0} pages vues, | |||
Expand,Développer, | |||
Collapse,Effondrer, | |||
Collapse,Réduire, | |||
"Invalid Bearer token, please provide a valid access token with prefix 'Bearer'.","Jeton de porteur non valide, veuillez fournir un jeton d'accès valide avec le préfixe «porteur».", | |||
"Failed to decode token, please provide a valid base64-encoded token.","Échec du décodage du jeton, veuillez fournir un jeton encodé en base64 valide.", | |||
"Invalid token, please provide a valid token with prefix 'Basic' or 'Token'.","Jeton non valide, veuillez fournir un jeton valide avec le préfixe «Basic» ou «Token».", | |||
@@ -4215,7 +4216,7 @@ since yesterday,depuis hier, | |||
since last week,depuis la semaine dernière, | |||
since last month,depuis le mois dernier, | |||
since last year,depuis l'année dernière, | |||
Show,Spectacle, | |||
Show,Afficher, | |||
New Number Card,Nouvelle carte de numéro, | |||
Your Shortcuts,Vos raccourcis, | |||
You haven't added any Dashboard Charts or Number Cards yet.,Vous n'avez pas encore ajouté de tableaux de bord ou de cartes numériques., | |||
@@ -4700,3 +4701,17 @@ Value cannot be negative for {0}: {1},La valeur ne peut pas être négative pour | |||
Negative Value,Valeur négative, | |||
Authentication failed while receiving emails from Email Account: {0}.,L'authentification a échoué lors de la réception des e-mails du compte de messagerie: {0}., | |||
Message from server: {0},Message du serveur: {0}, | |||
{0} edited this {1},{0} a édité {1}, | |||
{0} created this {1}, {0} a créé {1} | |||
Report an Issue, Signaler une anomalie | |||
About, A Propos | |||
My Profile, Mon profil | |||
My Settings, Mes paramètres | |||
Toggle Full Width, Changer l'affichage en pleine largeur | |||
Toggle Theme, Basculer le thème | |||
Theme Changed, Thème changé | |||
Amend, Nouv. version | |||
Document has been submitted, Document validé | |||
Document has been cancelled, Document annulé | |||
Document is in draft state, Document au statut brouillon | |||
Copy to Clipboard,Copier vers le presse-papiers |
@@ -1,224 +0,0 @@ | |||
# Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe, re, frappe.utils | |||
from frappe.desk.notifications import get_notifications | |||
from frappe import _ | |||
@frappe.whitelist() | |||
def get_bot_reply(question): | |||
return BotReply().get_reply(question) | |||
class BotParser(object): | |||
'''Base class for bot parser''' | |||
def __init__(self, reply, query): | |||
self.query = query | |||
self.reply = reply | |||
self.tables = reply.tables | |||
self.doctype_names = reply.doctype_names | |||
def has(self, *words): | |||
'''return True if any of the words is present int the query''' | |||
for word in words: | |||
if re.search(r'\b{0}\b'.format(word), self.query): | |||
return True | |||
def startswith(self, *words): | |||
'''return True if the query starts with any of the given words''' | |||
for w in words: | |||
if self.query.startswith(w): | |||
return True | |||
def strip_words(self, query, *words): | |||
'''Remove the given words from the query''' | |||
for word in words: | |||
query = re.sub(r'\b{0}\b'.format(word), '', query) | |||
return query.strip() | |||
def format_list(self, data): | |||
'''Format list as markdown''' | |||
return _('I found these:') + ' ' + ', '.join(' [{title}](/app/Form/{doctype}/{name})'.format( | |||
title = d.title or d.name, | |||
doctype=self.get_doctype(), | |||
name=d.name) for d in data) | |||
def get_doctype(self): | |||
'''returns the doctype name from self.tables''' | |||
return self.doctype_names[self.tables[0]] | |||
class ShowNotificationBot(BotParser): | |||
'''Show open notifications''' | |||
def get_reply(self): | |||
if self.has("whatsup", "what's up", "wassup", "whats up", 'notifications', 'open tasks'): | |||
n = get_notifications() | |||
open_items = sorted(n.get('open_count_doctype').items()) | |||
if open_items: | |||
return ("Following items need your attention:\n\n" | |||
+ "\n\n".join("{0} [{1}](/app/List/{1})".format(d[1], d[0]) | |||
for d in open_items if d[1] > 0)) | |||
else: | |||
return 'Take it easy, nothing urgent needs your attention' | |||
class GetOpenListBot(BotParser): | |||
'''Get list of open items''' | |||
def get_reply(self): | |||
if self.startswith('open', 'show open', 'list open', 'get open'): | |||
if self.tables: | |||
doctype = self.get_doctype() | |||
from frappe.desk.notifications import get_notification_config | |||
filters = get_notification_config().get('for_doctype').get(doctype, None) | |||
if filters: | |||
if isinstance(filters, dict): | |||
data = frappe.get_list(doctype, filters=filters) | |||
else: | |||
data = [{'name':d[0], 'title':d[1]} for d in frappe.get_attr(filters)(as_list=True)] | |||
return ", ".join('[{title}](/app/Form/{doctype}/{name})'.format(doctype=doctype, | |||
name=d.get('name'), title=d.get('title') or d.get('name')) for d in data) | |||
else: | |||
return _("Can't identify open {0}. Try something else.").format(doctype) | |||
class ListBot(BotParser): | |||
def get_reply(self): | |||
if self.query.endswith(' ' + _('list')) and self.startswith(_('list')): | |||
self.query = _('list') + ' ' + self.query.replace(' ' + _('list'), '') | |||
if self.startswith(_('list'), _('show')): | |||
like = None | |||
if ' ' + _('like') + ' ' in self.query: | |||
self.query, like = self.query.split(' ' + _('like') + ' ') | |||
self.tables = self.reply.identify_tables(self.query.split(None, 1)[1]) | |||
if self.tables: | |||
doctype = self.get_doctype() | |||
meta = frappe.get_meta(doctype) | |||
fields = ['name'] | |||
if meta.title_field: | |||
fields.append('`{0}` as title'.format(meta.title_field)) | |||
filters = {} | |||
if like: | |||
filters={ | |||
meta.title_field or 'name': ('like', '%' + like + '%') | |||
} | |||
return self.format_list(frappe.get_list(self.get_doctype(), fields=fields, filters=filters)) | |||
class CountBot(BotParser): | |||
def get_reply(self): | |||
if self.startswith('how many'): | |||
self.tables = self.reply.identify_tables(self.query.split(None, 1)[1]) | |||
if self.tables: | |||
return str(frappe.db.sql('select count(*) from `tab{0}`'.format(self.get_doctype()))[0][0]) | |||
class FindBot(BotParser): | |||
def get_reply(self): | |||
if self.startswith('find', 'search'): | |||
query = self.query.split(None, 1)[1] | |||
if self.has('from'): | |||
text, table = query.split('from') | |||
if self.has('in'): | |||
text, table = query.split('in') | |||
if table: | |||
text = text.strip() | |||
self.tables = self.reply.identify_tables(table.strip()) | |||
if self.tables: | |||
filters = {'name': ('like', '%{0}%'.format(text))} | |||
or_filters = None | |||
title_field = frappe.get_meta(self.get_doctype()).title_field | |||
if title_field and title_field!='name': | |||
or_filters = {'title': ('like', '%{0}%'.format(text))} | |||
data = frappe.get_list(self.get_doctype(), | |||
filters=filters, or_filters=or_filters) | |||
if data: | |||
return self.format_list(data) | |||
else: | |||
return _("Could not find {0} in {1}").format(text, self.get_doctype()) | |||
else: | |||
self.out = _("Could not identify {0}").format(table) | |||
else: | |||
self.out = _("You can find things by asking 'find orange in customers'").format(table) | |||
class BotReply(object): | |||
'''Build a reply for the bot by calling all parsers''' | |||
def __init__(self): | |||
self.tables = [] | |||
def get_reply(self, query): | |||
self.query = query.lower() | |||
self.setup() | |||
self.pre_process() | |||
# basic replies | |||
if self.query.split()[0] in ("hello", "hi"): | |||
return _("Hello {0}").format(frappe.utils.get_fullname()) | |||
if self.query == "help": | |||
return help_text.format(frappe.utils.get_fullname()) | |||
# build using parsers | |||
replies = [] | |||
for parser in frappe.get_hooks('bot_parsers'): | |||
reply = None | |||
try: | |||
reply = frappe.get_attr(parser)(self, query).get_reply() | |||
except frappe.PermissionError: | |||
reply = _("Oops, you are not allowed to know that") | |||
if reply: | |||
replies.append(reply) | |||
if replies: | |||
return '\n\n'.join(replies) | |||
if not reply: | |||
return _("Don't know, ask 'help'") | |||
def setup(self): | |||
self.setup_tables() | |||
self.identify_tables() | |||
def pre_process(self): | |||
if self.query.endswith("?"): | |||
self.query = self.query[:-1] | |||
if self.query in ("todo", "to do"): | |||
self.query = "open todo" | |||
def setup_tables(self): | |||
tables = frappe.get_all("DocType", {"istable": 0}) | |||
self.all_tables = [d.name.lower() for d in tables] | |||
self.doctype_names = {d.name.lower():d.name for d in tables} | |||
def identify_tables(self, query=None): | |||
if not query: | |||
query = self.query | |||
self.tables = [] | |||
for t in self.all_tables: | |||
if t in query or t[:-1] in query: | |||
self.tables.append(t) | |||
return self.tables | |||
help_text = """Hello {0}, I am a K.I.S.S Bot, not AI, so be kind. I can try answering a few questions like, | |||
- "todo": list my todos | |||
- "show customers": list customers | |||
- "show customers like giant": list customer containing giant | |||
- "locate shirt": find where to find item "shirt" | |||
- "open issues": find open issues, try "open sales orders" | |||
- "how many users": count number of users | |||
- "find asian in sales orders": find sales orders where name or title has "asian" | |||
have fun! | |||
""" |
@@ -1645,18 +1645,21 @@ def validate_json_string(string: str) -> None: | |||
raise frappe.ValidationError | |||
def get_user_info_for_avatar(user_id: str) -> Dict: | |||
user_info = { | |||
"email": user_id, | |||
"image": "", | |||
"name": user_id | |||
} | |||
try: | |||
user_info["email"] = frappe.get_cached_value("User", user_id, "email") | |||
user_info["name"] = frappe.get_cached_value("User", user_id, "full_name") | |||
user_info["image"] = frappe.get_cached_value("User", user_id, "user_image") | |||
except Exception: | |||
frappe.local.message_log = [] | |||
return user_info | |||
user = frappe.get_cached_doc("User", user_id) | |||
return { | |||
"email": user.email, | |||
"image": user.user_image, | |||
"name": user.full_name | |||
} | |||
except frappe.DoesNotExistError: | |||
frappe.clear_last_message() | |||
return { | |||
"email": user_id, | |||
"image": "", | |||
"name": user_id | |||
} | |||
def validate_python_code(string: str, fieldname=None, is_expression: bool = True) -> None: | |||
@@ -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 | |||
@@ -48,7 +48,8 @@ def validate_template(html): | |||
"""Throws exception if there is a syntax error in the Jinja Template""" | |||
import frappe | |||
from jinja2 import TemplateSyntaxError | |||
if not html: | |||
return | |||
jenv = get_jenv() | |||
try: | |||
jenv.from_string(html) | |||
@@ -1,151 +0,0 @@ | |||
import frappe | |||
import json, os | |||
from frappe.modules import scrub, get_module_path, utils | |||
from frappe.custom.doctype.customize_form.customize_form import doctype_properties, docfield_properties | |||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter | |||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field | |||
from frappe.core.page.permission_manager.permission_manager import get_standard_permissions | |||
from frappe.permissions import setup_custom_perms | |||
from urllib.request import urlopen | |||
branch = 'develop' | |||
def reset_all(): | |||
for doctype in frappe.db.get_all('DocType', dict(custom=0)): | |||
print(doctype.name) | |||
reset_doc(doctype.name) | |||
def reset_doc(doctype): | |||
''' | |||
doctype = name of the DocType that you want to reset | |||
''' | |||
# fetch module name | |||
module = frappe.db.get_value('DocType', doctype, 'module') | |||
app = utils.get_module_app(module) | |||
# get path for doctype's json and its equivalent git url | |||
doc_path = os.path.join(get_module_path(module), 'doctype', scrub(doctype), scrub(doctype)+'.json') | |||
try: | |||
git_link = '/'.join(['https://raw.githubusercontent.com/frappe',\ | |||
app, branch, doc_path.split('apps/'+app)[1]]) | |||
original_file = urlopen(git_link).read() | |||
except: | |||
print('Did not find {0} in {1}'.format(doctype, app)) | |||
return | |||
# load local and original json objects | |||
local_doc = json.loads(open(doc_path, 'r').read()) | |||
original_doc = json.loads(original_file) | |||
remove_duplicate_fields(doctype) | |||
set_property_setter(doctype, local_doc, original_doc) | |||
make_custom_fields(doctype, local_doc, original_doc) | |||
with open(doc_path, 'w+') as f: | |||
f.write(original_file) | |||
f.close() | |||
setup_perms_for(doctype) | |||
frappe.db.commit() | |||
def remove_duplicate_fields(doctype): | |||
for field in frappe.db.sql('''select fieldname, count(1) as cnt from tabDocField where parent=%s group by fieldname having cnt > 1''', doctype): | |||
frappe.db.sql('delete from tabDocField where fieldname=%s and parent=%s limit 1', (field[0], doctype)) | |||
print('removed duplicate {0} in {1}'.format(field[0], doctype)) | |||
def set_property_setter(doctype, local_doc, original_doc): | |||
''' compare doctype_properties and docfield_properties and create property_setter ''' | |||
# doctype_properties reset | |||
for dp in doctype_properties: | |||
# make property_setter to mimic changes made in local json | |||
if dp in local_doc and dp not in original_doc: | |||
make_property_setter(doctype, '', dp, local_doc[dp], doctype_properties[dp], for_doctype=True) | |||
local_fields = get_fields_dict(local_doc) | |||
original_fields = get_fields_dict(original_doc) | |||
# iterate through field and properties of each of those field | |||
for docfield in original_fields: | |||
for prop in original_fields[docfield]: | |||
# skip fields that are not in local_fields | |||
if docfield not in local_fields: continue | |||
if prop in docfield_properties and prop in local_fields[docfield]\ | |||
and original_fields[docfield][prop] != local_fields[docfield][prop]: | |||
# make property_setter equivalent of local changes | |||
make_property_setter(doctype, docfield, prop, local_fields[docfield][prop],\ | |||
docfield_properties[prop]) | |||
def make_custom_fields(doctype, local_doc, original_doc): | |||
''' | |||
check fields and create a custom field equivalent for non standard fields | |||
''' | |||
local_fields, original_fields = get_fields_dict(local_doc), get_fields_dict(original_doc) | |||
local_fields = sorted(local_fields.items(), key=lambda x: x[1]['idx']) | |||
doctype_doc = frappe.get_doc('DocType', doctype) | |||
custom_docfield_properties, prev = get_custom_docfield_properties(), "" | |||
for field, field_dict in local_fields: | |||
df = {} | |||
if field not in original_fields: | |||
for prop in field_dict: | |||
if prop in custom_docfield_properties: | |||
df[prop] = field_dict[prop] | |||
df['insert_after'] = prev if prev else '' | |||
doctype_doc.fields = [d for d in doctype_doc.fields if d.fieldname != df['fieldname']] | |||
doctype_doc.update_children() | |||
create_custom_field(doctype, df) | |||
# set current field as prev field for next field | |||
prev = field | |||
def get_fields_dict(doc): | |||
fields, idx = {}, 0 | |||
for field in doc['fields']: | |||
field['idx'] = idx | |||
fields[field.get('fieldname')] = field | |||
idx += 1 | |||
return fields | |||
def get_custom_docfield_properties(): | |||
fields_meta = frappe.get_meta('Custom Field').fields | |||
fields = {} | |||
for d in fields_meta: | |||
fields[d.fieldname] = d.fieldtype | |||
return fields | |||
def setup_perms_for(doctype): | |||
perms = frappe.get_all('DocPerm', fields='*', filters=dict(parent=doctype), order_by='idx asc') | |||
# get default perms | |||
try: | |||
standard_perms = get_standard_permissions(doctype) | |||
except (IOError, KeyError): | |||
# no json file, doctype no longer exists! | |||
return | |||
same = True | |||
if len(standard_perms) != len(perms): | |||
same = False | |||
else: | |||
for i, p in enumerate(perms): | |||
standard = standard_perms[i] | |||
for fieldname in frappe.get_meta('DocPerm').get_fieldnames_with_value(): | |||
if p.get(fieldname) != standard.get(fieldname): | |||
same = False | |||
break | |||
if not same: | |||
break | |||
if not same: | |||
setup_custom_perms(doctype) |
@@ -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,0 +1,5 @@ | |||
{{ frappe.render_template('templates/includes/image_with_blur.html', { | |||
"src": url, | |||
"alt": description, | |||
"class": "full-width-image" | |||
}) }} |
@@ -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", | |||
@@ -10,7 +10,7 @@ | |||
{%- set collapse_id = 'id-' + frappe.utils.generate_hash('Collapse', 12) -%} | |||
<a class="collapsible-title" data-toggle="collapse" href="#{{ collapse_id }}" role="button" | |||
aria-expanded="false" aria-controls="{{ collapse_id }}"> | |||
<h3>{{ item.title }}</h3> | |||
<div class="collapsible-item-title">{{ item.title }}</div> | |||
<svg class="collapsible-icon" width="24" height="24" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"> | |||
<path class="vertical" d="M8 4V12" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" | |||
stroke-linecap="round" | |||
@@ -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; | |||