Merge remote-tracking branch 'upstream/develop' into esbuild
This commit is contained in:
commit
6347d3e6f1
69 changed files with 928 additions and 257 deletions
|
|
@ -80,6 +80,7 @@
|
|||
"validate_email": true,
|
||||
"validate_name": true,
|
||||
"validate_phone": true,
|
||||
"validate_url": true,
|
||||
"get_number_format": true,
|
||||
"format_number": true,
|
||||
"format_currency": true,
|
||||
|
|
|
|||
65
cypress/fixtures/data_field_validation_doctype.js
Normal file
65
cypress/fixtures/data_field_validation_doctype.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
export default {
|
||||
name: 'Validation Test',
|
||||
custom: 1,
|
||||
actions: [],
|
||||
creation: '2019-03-15 06:29:07.215072',
|
||||
doctype: 'DocType',
|
||||
editable_grid: 1,
|
||||
engine: 'InnoDB',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'email',
|
||||
fieldtype: 'Data',
|
||||
label: 'Email',
|
||||
options: 'Email'
|
||||
},
|
||||
{
|
||||
fieldname: 'URL',
|
||||
fieldtype: 'Data',
|
||||
label: 'URL',
|
||||
options: 'URL'
|
||||
},
|
||||
{
|
||||
fieldname: 'Phone',
|
||||
fieldtype: 'Data',
|
||||
label: 'Phone',
|
||||
options: 'Phone'
|
||||
},
|
||||
{
|
||||
fieldname: 'person_name',
|
||||
fieldtype: 'Data',
|
||||
label: 'Person Name',
|
||||
options: 'Name'
|
||||
},
|
||||
{
|
||||
fieldname: 'read_only_url',
|
||||
fieldtype: 'Data',
|
||||
label: 'Read Only URL',
|
||||
options: 'URL',
|
||||
read_only: '1',
|
||||
default: 'https://frappe.io'
|
||||
}
|
||||
],
|
||||
issingle: 1,
|
||||
links: [],
|
||||
modified: '2021-04-19 14:40:53.127615',
|
||||
modified_by: 'Administrator',
|
||||
module: 'Custom',
|
||||
owner: 'Administrator',
|
||||
permissions: [
|
||||
{
|
||||
create: 1,
|
||||
delete: 1,
|
||||
email: 1,
|
||||
print: 1,
|
||||
read: 1,
|
||||
role: 'System Manager',
|
||||
share: 1,
|
||||
write: 1
|
||||
}
|
||||
],
|
||||
quick_entry: 1,
|
||||
sort_field: 'modified',
|
||||
sort_order: 'ASC',
|
||||
track_changes: 1
|
||||
};
|
||||
43
cypress/integration/data_field_form_validation.js
Normal file
43
cypress/integration/data_field_form_validation.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import data_field_validation_doctype from '../fixtures/data_field_validation_doctype';
|
||||
const doctype_name = data_field_validation_doctype.name;
|
||||
|
||||
|
||||
context('Data Field Input Validation in New Form', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
return cy.insert_doc('DocType', data_field_validation_doctype, true);
|
||||
});
|
||||
|
||||
function validateField(fieldname, invalid_value, valid_value) {
|
||||
// Invalid, should have has-error class
|
||||
cy.get_field(fieldname).clear().type(invalid_value).blur();
|
||||
cy.get(`.frappe-control[data-fieldname="${fieldname}"]`).should('have.class', 'has-error');
|
||||
// Valid value, should not have has-error class
|
||||
cy.get_field(fieldname).clear().type(valid_value);
|
||||
cy.get(`.frappe-control[data-fieldname="${fieldname}"]`).should('not.have.class', 'has-error');
|
||||
}
|
||||
|
||||
describe('Data Field Options', () => {
|
||||
it('should validate email address', () => {
|
||||
cy.new_form(doctype_name);
|
||||
validateField('email', 'captian', 'hello@test.com');
|
||||
});
|
||||
|
||||
it('should validate URL', () => {
|
||||
validateField('url', 'jkl', 'https://frappe.io');
|
||||
validateField('url', 'abcd.com', 'http://google.com/home');
|
||||
validateField('url', '&&http://google.uae', 'gopher://frappe.io');
|
||||
validateField('url', 'ftt2:://google.in?q=news', 'ftps2://frappe.io/__/#home');
|
||||
validateField('url', 'ftt2://', 'ntps://localhost'); // For intranet URLs
|
||||
});
|
||||
|
||||
it('should validate phone number', () => {
|
||||
validateField('phone', 'america', '89787878');
|
||||
});
|
||||
|
||||
it('should validate name', () => {
|
||||
validateField('person_name', ' 777Hello', 'James Bond');
|
||||
});
|
||||
});
|
||||
});
|
||||
43
cypress/integration/url_data_field.js
Normal file
43
cypress/integration/url_data_field.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import data_field_validation_doctype from '../fixtures/data_field_validation_doctype';
|
||||
|
||||
const doctype_name = data_field_validation_doctype.name;
|
||||
|
||||
context('URL Data Field Input', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
return cy.insert_doc('DocType', data_field_validation_doctype, true);
|
||||
});
|
||||
|
||||
|
||||
describe('URL Data Field Input ', () => {
|
||||
it('should not show URL link button without focus', () => {
|
||||
cy.new_form(doctype_name);
|
||||
cy.get_field('url').clear().type('https://frappe.io');
|
||||
cy.get_field('url').blur().wait(500);
|
||||
cy.get('.link-btn').should('not.be.visible');
|
||||
});
|
||||
|
||||
it('should show URL link button on focus', () => {
|
||||
cy.get_field('url').focus().wait(500);
|
||||
cy.get('.link-btn').should('be.visible');
|
||||
});
|
||||
|
||||
it('should not show URL link button for invalid URL', () => {
|
||||
cy.get_field('url').clear().type('fuzzbuzz');
|
||||
cy.get('.link-btn').should('not.be.visible');
|
||||
});
|
||||
|
||||
it('should have valid URL link with target _blank', () => {
|
||||
cy.get_field('url').clear().type('https://frappe.io');
|
||||
cy.get('.link-btn .btn-open').should('have.attr', 'href', 'https://frappe.io');
|
||||
cy.get('.link-btn .btn-open').should('have.attr', 'target', '_blank');
|
||||
});
|
||||
|
||||
it('should inject anchor tag in read-only URL data field', () => {
|
||||
cy.get('[data-fieldname="read_only_url"]')
|
||||
.find('a')
|
||||
.should('have.attr', 'target', '_blank');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -201,12 +201,20 @@ def handle_exception(e):
|
|||
response = None
|
||||
http_status_code = getattr(e, "http_status_code", 500)
|
||||
return_as_message = False
|
||||
accept_header = frappe.get_request_header("Accept") or ""
|
||||
respond_as_json = (
|
||||
frappe.get_request_header('Accept')
|
||||
and (frappe.local.is_ajax or 'application/json' in accept_header)
|
||||
or (
|
||||
frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text")
|
||||
)
|
||||
)
|
||||
|
||||
if frappe.conf.get('developer_mode'):
|
||||
# don't fail silently
|
||||
print(frappe.get_traceback())
|
||||
|
||||
if frappe.get_request_header('Accept') and (frappe.local.is_ajax or 'application/json' in frappe.get_request_header('Accept')):
|
||||
if respond_as_json:
|
||||
# handle ajax responses first
|
||||
# if the request is ajax, send back the trace or error message
|
||||
response = frappe.utils.response.report_error(http_status_code)
|
||||
|
|
|
|||
|
|
@ -42,8 +42,6 @@ def get_bootinfo():
|
|||
bootinfo.user_info = get_user_info()
|
||||
bootinfo.sid = frappe.session['sid']
|
||||
|
||||
bootinfo.user_groups = frappe.get_all('User Group', pluck="name")
|
||||
|
||||
bootinfo.modules = {}
|
||||
bootinfo.module_list = []
|
||||
load_desktop_data(bootinfo)
|
||||
|
|
|
|||
32
frappe/change_log/v13/v13_2_0.md
Normal file
32
frappe/change_log/v13/v13_2_0.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Version 13.2.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- Add option to mention a group of users ([#12844](https://github.com/frappe/frappe/pull/12844))
|
||||
- Copy DocType / documents across sites ([#12872](https://github.com/frappe/frappe/pull/12872))
|
||||
- Scheduler log in notifications ([#1135](https://github.com/frappe/frappe/pull/1135))
|
||||
- Add Enable/Disable Webhook via Check Field ([#12842](https://github.com/frappe/frappe/pull/12842))
|
||||
- Allow query/custom reports to save custom data in the json field ([#12534](https://github.com/frappe/frappe/pull/12534))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Load server translations in boot (backport #12848) ([#12852](https://github.com/frappe/frappe/pull/12852))
|
||||
- Allow to override dashboard chart properties type/color ([#12846](https://github.com/frappe/frappe/pull/12846))
|
||||
- Multi-column paste in grid ([#12861](https://github.com/frappe/frappe/pull/12861))
|
||||
- Add log_error and FrappeClient to restricted python ([#12857](https://github.com/frappe/frappe/pull/12857))
|
||||
- Redirect Web Form user directly to success URL, if no amount is due ([#12661](https://github.com/frappe/frappe/pull/12661))
|
||||
- Attachment pill lock icon redirects to File ([#12864](https://github.com/frappe/frappe/pull/12864))
|
||||
- Redirect Web Form user directly to success URL, if no amount is due (backport #12661) ([#12856](https://github.com/frappe/frappe/pull/12856))
|
||||
- Remove events to redraw charts ([#12973](https://github.com/frappe/frappe/pull/12973))
|
||||
- Don't allow user to remove/change data source file in data import ([#12827](https://github.com/frappe/frappe/pull/12827))
|
||||
- Load server translations in boot ([#12848](https://github.com/frappe/frappe/pull/12848))
|
||||
- Newly created Workspace not being accessible unless a shortcut u… ([#12866](https://github.com/frappe/frappe/pull/12866))
|
||||
- Currency labels in grids ([#12974](https://github.com/frappe/frappe/pull/12974))
|
||||
- Handle error while session start ([#12933](https://github.com/frappe/frappe/pull/12933))
|
||||
- Add field type check in custom field validation ([#12858](https://github.com/frappe/frappe/pull/12858))
|
||||
- Make language select optional and fix breakpoint issues ([#12860](https://github.com/frappe/frappe/pull/12860))
|
||||
- Form Dashboard reference link ([#12945](https://github.com/frappe/frappe/pull/12945))
|
||||
- Invalid HTML generated by the base template ([#12953](https://github.com/frappe/frappe/pull/12953))
|
||||
- Default values were not triggering change event ([#12975](https://github.com/frappe/frappe/pull/12975))
|
||||
- Make strings translatable ([#12877](https://github.com/frappe/frappe/pull/12877))
|
||||
- Added build-message-files command ([#12950](https://github.com/frappe/frappe/pull/12950))
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
# imports - standard imports
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
# imports - third party imports
|
||||
import click
|
||||
|
|
@ -202,10 +203,13 @@ def install_app(context, apps):
|
|||
|
||||
|
||||
@click.command("list-apps")
|
||||
@click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text")
|
||||
@pass_context
|
||||
def list_apps(context):
|
||||
def list_apps(context, format):
|
||||
"List apps in site"
|
||||
|
||||
summary_dict = {}
|
||||
|
||||
def fix_whitespaces(text):
|
||||
if site == context.sites[-1]:
|
||||
text = text.rstrip()
|
||||
|
|
@ -234,18 +238,25 @@ def list_apps(context):
|
|||
]
|
||||
applications_summary = "\n".join(installed_applications)
|
||||
summary = f"{site_title}\n{applications_summary}\n"
|
||||
summary_dict[site] = [app.app_name for app in apps]
|
||||
|
||||
else:
|
||||
applications_summary = "\n".join(frappe.get_installed_apps())
|
||||
installed_applications = frappe.get_installed_apps()
|
||||
applications_summary = "\n".join(installed_applications)
|
||||
summary = f"{site_title}\n{applications_summary}\n"
|
||||
summary_dict[site] = installed_applications
|
||||
|
||||
summary = fix_whitespaces(summary)
|
||||
|
||||
if applications_summary and summary:
|
||||
if format == "text" and applications_summary and summary:
|
||||
print(summary)
|
||||
|
||||
frappe.destroy()
|
||||
|
||||
if format == "json":
|
||||
import json
|
||||
|
||||
click.echo(json.dumps(summary_dict))
|
||||
|
||||
@click.command('add-system-manager')
|
||||
@click.argument('email')
|
||||
|
|
@ -547,7 +558,7 @@ def move(dest_dir, site):
|
|||
site_dump_exists = os.path.exists(final_new_path)
|
||||
count = int(count or 0) + 1
|
||||
|
||||
os.rename(old_path, final_new_path)
|
||||
shutil.move(old_path, final_new_path)
|
||||
frappe.destroy()
|
||||
return final_new_path
|
||||
|
||||
|
|
|
|||
|
|
@ -531,7 +531,7 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
|
|||
|
||||
# Generate coverage report only for app that is being tested
|
||||
source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
|
||||
cov = Coverage(source=[source_path], omit=[
|
||||
omit=[
|
||||
'*.html',
|
||||
'*.js',
|
||||
'*.xml',
|
||||
|
|
@ -541,7 +541,12 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
|
|||
'*.vue',
|
||||
'*/doctype/*/*_dashboard.py',
|
||||
'*/patches/*'
|
||||
])
|
||||
]
|
||||
|
||||
if not app or app == 'frappe':
|
||||
omit.append('*/commands/*')
|
||||
|
||||
cov = Coverage(source=[source_path], omit=omit)
|
||||
cov.start()
|
||||
|
||||
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
|
||||
|
|
|
|||
|
|
@ -662,4 +662,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ class User(Document):
|
|||
|
||||
def after_insert(self):
|
||||
create_notification_settings(self.name)
|
||||
frappe.cache().delete_key('users_for_mentions')
|
||||
|
||||
def validate(self):
|
||||
self.check_demo()
|
||||
|
|
@ -129,6 +130,9 @@ class User(Document):
|
|||
if self.time_zone:
|
||||
frappe.defaults.set_default("time_zone", self.time_zone, self.name)
|
||||
|
||||
if self.has_value_changed('allow_in_mentions') or self.has_value_changed('user_type'):
|
||||
frappe.cache().delete_key('users_for_mentions')
|
||||
|
||||
def has_website_permission(self, ptype, user, verbose=False):
|
||||
"""Returns true if current user is the session user"""
|
||||
return self.name == frappe.session.user
|
||||
|
|
@ -389,6 +393,9 @@ class User(Document):
|
|||
# delete notification settings
|
||||
frappe.delete_doc("Notification Settings", self.name, ignore_permissions=True)
|
||||
|
||||
if self.get('allow_in_mentions'):
|
||||
frappe.cache().delete_key('users_for_mentions')
|
||||
|
||||
|
||||
def before_rename(self, old_name, new_name, merge=False):
|
||||
self.check_demo()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import frappe
|
|||
|
||||
class UserGroup(Document):
|
||||
def after_insert(self):
|
||||
frappe.publish_realtime('user_group_added', self.name)
|
||||
frappe.cache().delete_key('user_groups')
|
||||
|
||||
def on_trash(self):
|
||||
frappe.publish_realtime('user_group_deleted', self.name)
|
||||
frappe.cache().delete_key('user_groups')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
frappe.pages['recorder'].on_page_load = function(wrapper) {
|
||||
frappe.ui.make_app_page({
|
||||
parent: wrapper,
|
||||
title: 'Recorder',
|
||||
title: __('Recorder'),
|
||||
single_column: true,
|
||||
card_layout: true
|
||||
});
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@
|
|||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "doc_type",
|
||||
"fieldname": "naming_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Naming"
|
||||
|
|
@ -287,6 +288,16 @@
|
|||
"fieldname": "autoname",
|
||||
"fieldtype": "Data",
|
||||
"label": "Auto Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_email_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Email Template",
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_26",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
|
|
@ -295,7 +306,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-22 12:27:15.462727",
|
||||
"modified": "2021-04-29 21:21:06.476372",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form",
|
||||
|
|
@ -316,4 +327,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
.download-backup-card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
margin-bottom: var(--margin-lg);
|
||||
}
|
||||
|
||||
.download-backup-card:hover {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
frappe.pages['backups'].on_page_load = function(wrapper) {
|
||||
var page = frappe.ui.make_app_page({
|
||||
parent: wrapper,
|
||||
title: 'Download Backups',
|
||||
title: __('Download Backups'),
|
||||
single_column: true
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
frappe.pages['translation-tool'].on_page_load = function(wrapper) {
|
||||
var page = frappe.ui.make_app_page({
|
||||
parent: wrapper,
|
||||
title: 'Translation Tool',
|
||||
title: __('Translation Tool'),
|
||||
single_column: true,
|
||||
card_layout: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
<div class="chart-wrapper performance-heatmap">
|
||||
<div class="null-state">
|
||||
<span>No Data to Show</span>
|
||||
<span>{%=__("No Data to Show") %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
</div>
|
||||
<div class="chart-wrapper performance-percentage-chart">
|
||||
<div class="null-state">
|
||||
<span>No Data to Show</span>
|
||||
<span>{%=__("No Data to Show") %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
</div>
|
||||
<div class="chart-wrapper performance-line-chart">
|
||||
<div class="null-state">
|
||||
<span>No Data to Show</span>
|
||||
<span>{%=__("No Data to Show") %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -41,4 +41,4 @@
|
|||
<div class="recent-activity-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -221,3 +221,37 @@ def validate_and_sanitize_search_inputs(fn, instance, args, kwargs):
|
|||
return []
|
||||
|
||||
return fn(**kwargs)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_names_for_mentions(search_term):
|
||||
users_for_mentions = frappe.cache().get_value('users_for_mentions', get_users_for_mentions)
|
||||
user_groups = frappe.cache().get_value('user_groups', get_user_groups)
|
||||
|
||||
filtered_mentions = []
|
||||
for mention_data in users_for_mentions + user_groups:
|
||||
if search_term.lower() not in mention_data.value.lower():
|
||||
continue
|
||||
|
||||
mention_data['link'] = frappe.utils.get_url_to_form(
|
||||
'User Group' if mention_data.get('is_group') else 'User Profile',
|
||||
mention_data['id']
|
||||
)
|
||||
|
||||
filtered_mentions.append(mention_data)
|
||||
|
||||
return sorted(filtered_mentions, key=lambda d: d['value'])
|
||||
|
||||
def get_users_for_mentions():
|
||||
return frappe.get_all('User',
|
||||
fields=['name as id', 'full_name as value'],
|
||||
filters={
|
||||
'name': ['not in', ('Administrator', 'Guest')],
|
||||
'allowed_in_mentions': True,
|
||||
'user_type': 'System User',
|
||||
})
|
||||
|
||||
def get_user_groups():
|
||||
return frappe.get_all('User Group', fields=['name as id', 'name as value'], update={
|
||||
'is_group': True
|
||||
})
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ class EventProducer(Document):
|
|||
self.reload()
|
||||
|
||||
def check_url(self):
|
||||
if not frappe.utils.validate_url(self.producer_url):
|
||||
frappe.throw(_('Invalid URL'))
|
||||
valid_url_schemes = ("http", "https")
|
||||
frappe.utils.validate_url(self.producer_url, throw=True, valid_schemes=valid_url_schemes)
|
||||
|
||||
# remove '/' from the end of the url like http://test_site.com/
|
||||
# to prevent mismatch in get_url() results
|
||||
|
|
|
|||
|
|
@ -71,7 +71,8 @@ numeric_fieldtypes = (
|
|||
data_field_options = (
|
||||
'Email',
|
||||
'Name',
|
||||
'Phone'
|
||||
'Phone',
|
||||
'URL'
|
||||
)
|
||||
|
||||
default_fields = (
|
||||
|
|
|
|||
|
|
@ -666,6 +666,12 @@ class BaseDocument(object):
|
|||
if data_field_options == "Phone":
|
||||
frappe.utils.validate_phone_number(data, throw=True)
|
||||
|
||||
if data_field_options == "URL":
|
||||
if not data:
|
||||
continue
|
||||
|
||||
frappe.utils.validate_url(data, throw=True)
|
||||
|
||||
def _validate_constants(self):
|
||||
if frappe.flags.in_import or self.is_new() or self.flags.ignore_validate_constants:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -599,9 +599,10 @@ def get_client_scopes(client_id):
|
|||
def get_userinfo(user):
|
||||
picture = None
|
||||
frappe_server_url = get_server_url()
|
||||
valid_url_schemes = ("http", "https", "ftp", "ftps")
|
||||
|
||||
if user.user_image:
|
||||
if frappe.utils.validate_url(user.user_image):
|
||||
if frappe.utils.validate_url(user.user_image, valid_schemes=valid_url_schemes):
|
||||
picture = user.user_image
|
||||
else:
|
||||
picture = frappe_server_url + "/" + user.user_image
|
||||
|
|
|
|||
|
|
@ -114,8 +114,6 @@ frappe.Application = class Application {
|
|||
dialog.get_close_btn().toggle(false);
|
||||
});
|
||||
|
||||
this.setup_user_group_listeners();
|
||||
|
||||
// listen to build errors
|
||||
this.setup_build_events();
|
||||
|
||||
|
|
@ -591,15 +589,6 @@ frappe.Application = class Application {
|
|||
}
|
||||
}
|
||||
|
||||
setup_user_group_listeners() {
|
||||
frappe.realtime.on('user_group_added', (user_group) => {
|
||||
frappe.boot.user_groups && frappe.boot.user_groups.push(user_group);
|
||||
});
|
||||
frappe.realtime.on('user_group_deleted', (user_group) => {
|
||||
frappe.boot.user_groups = (frappe.boot.user_groups || []).filter(el => el !== user_group);
|
||||
});
|
||||
}
|
||||
|
||||
setup_energy_point_listeners() {
|
||||
frappe.realtime.on('energy_point_alert', (message) => {
|
||||
frappe.show_alert(message);
|
||||
|
|
@ -609,8 +598,7 @@ frappe.Application = class Application {
|
|||
setup_copy_doc_listener() {
|
||||
$('body').on('paste', (e) => {
|
||||
try {
|
||||
let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
|
||||
let pasted_data = clipboard_data.getData('Text');
|
||||
let pasted_data = frappe.utils.get_clipboard_data(e);
|
||||
let doc = JSON.parse(pasted_data);
|
||||
if (doc.doctype) {
|
||||
e.preventDefault();
|
||||
|
|
@ -625,6 +613,7 @@ frappe.Application = class Application {
|
|||
let res = frappe.model.with_doctype(doc.doctype, () => {
|
||||
let newdoc = frappe.model.copy_doc(doc);
|
||||
newdoc.__newname = doc.name;
|
||||
delete doc.name;
|
||||
newdoc.idx = null;
|
||||
newdoc.__run_link_triggers = false;
|
||||
frappe.set_route('Form', newdoc.doctype, newdoc.name);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import Picker from '../../color_picker/color_picker';
|
||||
|
||||
frappe.ui.form.ControlColor = class ControlColor extends frappe.ui.form.ControlData {
|
||||
make_input () {
|
||||
make_input() {
|
||||
this.df.placeholder = this.df.placeholder || __('Choose a color');
|
||||
super.make_input();
|
||||
this.make_color_input();
|
||||
}
|
||||
make_color_input () {
|
||||
|
||||
make_color_input() {
|
||||
let picker_wrapper = $('<div>');
|
||||
this.picker = new Picker({
|
||||
parent: picker_wrapper[0],
|
||||
|
|
@ -48,7 +50,16 @@ frappe.ui.form.ControlColor = class ControlColor extends frappe.ui.form.ControlD
|
|||
$(window).off('hashchange.color-popover');
|
||||
});
|
||||
|
||||
this.$wrapper.find('.control-input').on('click', (e) => {
|
||||
this.picker.on_change = (color) => {
|
||||
this.set_value(color);
|
||||
};
|
||||
|
||||
if (!this.selected_color) {
|
||||
this.selected_color = $(`<div class="selected-color"></div>`);
|
||||
this.selected_color.insertAfter(this.$input);
|
||||
}
|
||||
|
||||
this.$wrapper.find('.selected-color').parent().on('click', (e) => {
|
||||
this.$wrapper.popover('toggle');
|
||||
if (!this.get_color()) {
|
||||
this.$input.val('');
|
||||
|
|
@ -63,16 +74,8 @@ frappe.ui.form.ControlColor = class ControlColor extends frappe.ui.form.ControlD
|
|||
this.$wrapper.popover('hide');
|
||||
});
|
||||
});
|
||||
|
||||
this.picker.on_change = (color) => {
|
||||
this.set_value(color);
|
||||
};
|
||||
|
||||
if (!this.selected_color) {
|
||||
this.selected_color = $(`<div class="selected-color"></div>`);
|
||||
this.selected_color.insertAfter(this.$input);
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
super.refresh();
|
||||
let color = this.get_color();
|
||||
|
|
@ -81,19 +84,21 @@ frappe.ui.form.ControlColor = class ControlColor extends frappe.ui.form.ControlD
|
|||
this.picker.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
set_formatted_input(value) {
|
||||
super.set_formatted_input(value);
|
||||
|
||||
this.$input.val(value || __('Choose a color'));
|
||||
this.$input.val(value);
|
||||
this.selected_color.css({
|
||||
"background-color": value || 'transparent',
|
||||
});
|
||||
this.selected_color.toggleClass('no-value', !value);
|
||||
}
|
||||
|
||||
get_color() {
|
||||
return this.validate(this.get_value());
|
||||
}
|
||||
validate (value) {
|
||||
|
||||
validate(value) {
|
||||
if (value === '') {
|
||||
return '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,35 +78,25 @@ frappe.ui.form.ControlComment = class ControlComment extends frappe.ui.form.Cont
|
|||
}
|
||||
|
||||
get_mention_options() {
|
||||
if (!(this.mentions && this.mentions.length)) {
|
||||
if (!this.enable_mentions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const at_values = this.mentions.slice();
|
||||
|
||||
let me = this;
|
||||
return {
|
||||
allowedChars: /^[A-Za-z0-9_]*$/,
|
||||
mentionDenotationChars: ["@"],
|
||||
isolateCharacter: true,
|
||||
source: function (searchTerm, renderList, mentionChar) {
|
||||
let values;
|
||||
|
||||
if (mentionChar === "@") {
|
||||
values = at_values;
|
||||
}
|
||||
|
||||
if (searchTerm.length === 0) {
|
||||
renderList(values, searchTerm);
|
||||
} else {
|
||||
const matches = [];
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (~values[i].value.toLowerCase().indexOf(searchTerm.toLowerCase())) {
|
||||
matches.push(values[i]);
|
||||
}
|
||||
}
|
||||
renderList(matches, searchTerm);
|
||||
}
|
||||
},
|
||||
source: frappe.utils.debounce(async function(search_term, renderList) {
|
||||
let method = me.mention_search_method || 'frappe.desk.search.get_names_for_mentions';
|
||||
let values = await frappe.xcall(method, {
|
||||
search_term
|
||||
});
|
||||
renderList(values, search_term);
|
||||
}, 300),
|
||||
renderItem(item) {
|
||||
let value = item.value;
|
||||
return `${value} ${item.is_group ? frappe.utils.icon('users') : ''}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
frappe.ui.form.ControlCurrency = class ControlCurrency extends frappe.ui.form.ControlFloat {
|
||||
format_for_input(value) {
|
||||
var formatted_value = format_number(value, this.get_number_format(), this.get_precision());
|
||||
return isNaN(parseFloat(value)) ? "" : formatted_value;
|
||||
return isNaN(Number(value)) ? "" : formatted_value;
|
||||
}
|
||||
|
||||
get_precision() {
|
||||
|
|
|
|||
|
|
@ -18,12 +18,99 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp
|
|||
this.$input.attr("maxlength", this.df.length || 140);
|
||||
}
|
||||
|
||||
this.$input.on('paste', (e) => {
|
||||
let pasted_data = frappe.utils.get_clipboard_data(e);
|
||||
let maxlength = this.$input.attr('maxlength');
|
||||
if (maxlength && pasted_data.length > maxlength) {
|
||||
let warning_message = __('The value you pasted was {0} characters long. Max allowed characters is {1}.', [
|
||||
cstr(pasted_data.length).bold(),
|
||||
cstr(maxlength).bold()
|
||||
]);
|
||||
|
||||
// Only show edit link to users who can update the doctype
|
||||
if (this.frm && frappe.model.can_write(this.frm.doctype)) {
|
||||
let doctype_edit_link = null;
|
||||
if (this.frm.meta.custom) {
|
||||
doctype_edit_link = frappe.utils.get_form_link(
|
||||
'DocType',
|
||||
this.frm.doctype, true,
|
||||
__('this form')
|
||||
);
|
||||
} else {
|
||||
doctype_edit_link = frappe.utils.get_form_link('Customize Form', 'Customize Form', true, null, {
|
||||
doc_type: this.frm.doctype
|
||||
});
|
||||
}
|
||||
let edit_note = __('{0}: You can increase the limit for the field if required via {1}', [
|
||||
__('Note').bold(),
|
||||
doctype_edit_link
|
||||
]);
|
||||
warning_message += `<br><br><span class="text-muted text-small">${edit_note}</span>`;
|
||||
}
|
||||
|
||||
frappe.msgprint({
|
||||
message: warning_message,
|
||||
indicator: 'orange',
|
||||
title: __('Data Clipped')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.set_input_attributes();
|
||||
this.input = this.$input.get(0);
|
||||
this.has_input = true;
|
||||
this.bind_change_event();
|
||||
this.setup_autoname_check();
|
||||
|
||||
if (this.df.options == 'URL') {
|
||||
this.setup_url_field();
|
||||
}
|
||||
}
|
||||
|
||||
setup_url_field() {
|
||||
this.$wrapper.find('.control-input').append(
|
||||
`<span class="link-btn">
|
||||
<a class="btn-open no-decoration" title="${__("Open Link")}" target="_blank">
|
||||
${frappe.utils.icon('link-url', 'sm')}
|
||||
</a>
|
||||
</span>`
|
||||
);
|
||||
|
||||
this.$link = this.$wrapper.find('.link-btn');
|
||||
this.$link_open = this.$link.find('.btn-open');
|
||||
|
||||
this.$input.on("focus", () => {
|
||||
setTimeout(() => {
|
||||
let inputValue = this.get_input_value();
|
||||
|
||||
if (inputValue && validate_url(inputValue)) {
|
||||
this.$link.toggle(true);
|
||||
this.$link_open.attr('href', this.get_input_value());
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
|
||||
this.$input.bind("input", () => {
|
||||
let inputValue = this.get_input_value();
|
||||
|
||||
if (inputValue && validate_url(inputValue)) {
|
||||
this.$link.toggle(true);
|
||||
this.$link_open.attr('href', this.get_input_value());
|
||||
} else {
|
||||
this.$link.toggle(false);
|
||||
}
|
||||
});
|
||||
|
||||
this.$input.on("blur", () => {
|
||||
// if this disappears immediately, the user's click
|
||||
// does not register, hence timeout
|
||||
setTimeout(() => {
|
||||
this.$link.toggle(false);
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
bind_change_event() {
|
||||
const change_handler = e => {
|
||||
if (this.change) this.change(e);
|
||||
|
|
@ -126,6 +213,9 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp
|
|||
this.df.invalid = email_invalid;
|
||||
return v;
|
||||
}
|
||||
} else if (this.df.options == 'URL') {
|
||||
this.df.invalid = !validate_url(v);
|
||||
return v;
|
||||
} else {
|
||||
return v;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ frappe.ui.form.ControlFloat = class ControlFloat extends frappe.ui.form.ControlI
|
|||
number_format = this.get_number_format();
|
||||
}
|
||||
var formatted_value = format_number(value, number_format, this.get_precision());
|
||||
return isNaN(parseFloat(value)) ? "" : formatted_value;
|
||||
return isNaN(Number(value)) ? "" : formatted_value;
|
||||
}
|
||||
|
||||
get_number_format() {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class MentionBlot extends Embed {
|
|||
denotationChar.innerHTML = data.denotationChar;
|
||||
node.appendChild(denotationChar);
|
||||
node.innerHTML += data.value;
|
||||
node.innerHTML += `${data.isGroup === 'true' ? frappe.utils.icon('users') : ''}`;
|
||||
node.dataset.id = data.id;
|
||||
node.dataset.value = data.value;
|
||||
node.dataset.denotationChar = data.denotationChar;
|
||||
|
|
|
|||
|
|
@ -26,8 +26,7 @@ frappe.ui.form.ControlTable = class ControlTable extends frappe.ui.form.Control
|
|||
const row_docname = $(e.target).closest('.grid-row').data('name');
|
||||
const in_grid_form = $(e.target).closest('.form-in-grid').length;
|
||||
|
||||
let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
|
||||
let pasted_data = clipboard_data.getData('Text');
|
||||
let pasted_data = frappe.utils.get_clipboard_data(e);
|
||||
|
||||
if (!pasted_data || in_grid_form) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ frappe.ui.form.Footer = class FormFooter {
|
|||
parent: this.wrapper.find(".comment-box"),
|
||||
render_input: true,
|
||||
only_input: true,
|
||||
mentions: frappe.utils.get_names_for_mentions(),
|
||||
enable_mentions: true,
|
||||
df: {
|
||||
fieldtype: 'Comment',
|
||||
fieldname: 'comment'
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ class FormTimeline extends BaseTimeline {
|
|||
fieldname: 'comment',
|
||||
label: 'Comment'
|
||||
},
|
||||
mentions: frappe.utils.get_names_for_mentions(),
|
||||
enable_mentions: true,
|
||||
render_input: true,
|
||||
only_input: true,
|
||||
no_wrapper: true
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@ frappe.form.formatters = {
|
|||
return "<div style='text-align: right'>" + value + "</div>";
|
||||
}
|
||||
},
|
||||
Data: function(value) {
|
||||
Data: function(value, df) {
|
||||
if (df && df.options == "URL") {
|
||||
return `<a href="${value}" title="Open Link" target="_blank">${value}</a>`;
|
||||
}
|
||||
return value==null ? "" : value;
|
||||
},
|
||||
Select: function(value) {
|
||||
|
|
@ -156,7 +159,7 @@ frappe.form.formatters = {
|
|||
return value || "";
|
||||
},
|
||||
DateRange: function(value) {
|
||||
if($.isArray(value)) {
|
||||
if (Array.isArray(value)) {
|
||||
return __("{0} to {1}", [frappe.datetime.str_to_user(value[0]), frappe.datetime.str_to_user(value[1])]);
|
||||
} else {
|
||||
return value || "";
|
||||
|
|
@ -292,12 +295,12 @@ frappe.form.formatters = {
|
|||
return formatted_values.join(', ');
|
||||
},
|
||||
Color: (value) => {
|
||||
return `<div>
|
||||
return value ? `<div>
|
||||
<div class="selected-color" style="background-color: ${value}"></div>
|
||||
<span class="color-value">${value}</span>
|
||||
</div>`;
|
||||
</div>` : '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
frappe.form.get_formatter = function(fieldtype) {
|
||||
if(!fieldtype)
|
||||
|
|
|
|||
|
|
@ -422,7 +422,7 @@ export default class GridRow {
|
|||
field.$input
|
||||
.addClass('input-sm')
|
||||
.attr('data-col-idx', column.column_index)
|
||||
.attr('placeholder', __(df.label));
|
||||
.attr('placeholder', __(df.placeholder || df.label));
|
||||
// flag list input
|
||||
if (this.columns_list && this.columns_list.slice(-1)[0]===column) {
|
||||
field.$input.attr('data-last-input', 1);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
}
|
||||
set_title() {
|
||||
if (this.frm.is_new()) {
|
||||
var title = __('New {0}', [this.frm.meta.name]);
|
||||
var title = __('New {0}', [__(this.frm.meta.name)]);
|
||||
} else if (this.frm.meta.title_field) {
|
||||
let title_field = (this.frm.doc[this.frm.meta.title_field] || "").toString().trim();
|
||||
var title = strip_html(title_field || this.frm.docname);
|
||||
|
|
@ -551,7 +551,7 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
|
||||
let fields = this.frm.fields
|
||||
.filter(visible_fields_filter)
|
||||
.map(f => ({ label: f.df.label, value: f.df.fieldname }));
|
||||
.map(f => ({ label: __(f.df.label), value: f.df.fieldname }));
|
||||
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __('Jump to field'),
|
||||
|
|
|
|||
|
|
@ -150,13 +150,13 @@ frappe.views.ListViewSelect = class ListViewSelect {
|
|||
const views_wrapper = this.sidebar.sidebar.find(".views-section");
|
||||
views_wrapper.find(".sidebar-label").html(`${__(view)}`);
|
||||
const $dropdown = views_wrapper.find(".views-dropdown");
|
||||
|
||||
let placeholder = `Select ${view}`;
|
||||
|
||||
let placeholder = `${__("Select {0}", [__(view)])}`;
|
||||
let html = ``;
|
||||
|
||||
if (!items || !items.length) {
|
||||
html = `<div class="empty-state">
|
||||
${__("No {} Found", [view])}
|
||||
${__("No {0} Found", [__(view)])}
|
||||
</div>`;
|
||||
} else {
|
||||
const page_name = this.get_page_name();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<div class="tag-filters-area">
|
||||
<div class="active-tag-filters">
|
||||
<button class="btn btn-default btn-xs add-filter text-muted">
|
||||
Add Filter
|
||||
{{ __("Add Filter") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -71,12 +71,12 @@
|
|||
</div>
|
||||
<div v-if="requests.length == 0" class="no-result text-muted flex justify-center align-center" style="">
|
||||
<div class="msg-box no-border" v-if="status.status == 'Inactive'" >
|
||||
<p>Recorder is Inactive</p>
|
||||
<p><button class="btn btn-primary btn-sm btn-new-doc" @click="start()">Start Recording</button></p>
|
||||
<p>{{ __("Recorder is Inactive") }}</p>
|
||||
<p><button class="btn btn-primary btn-sm btn-new-doc" @click="start()">{{ __("Start Recording") }}</button></p>
|
||||
</div>
|
||||
<div class="msg-box no-border" v-if="status.status == 'Active'" >
|
||||
<p>No Requests found</p>
|
||||
<p>Go make some noise</p>
|
||||
<p>{{ __("No Requests found") }}</p>
|
||||
<p>{{ __("Go make some noise") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="requests.length != 0" class="list-paging-area">
|
||||
|
|
@ -108,12 +108,12 @@ export default {
|
|||
return {
|
||||
requests: [],
|
||||
columns: [
|
||||
{label: "Path", slug: "path"},
|
||||
{label: "Duration (ms)", slug: "duration", sortable: true, number: true},
|
||||
{label: "Time in Queries (ms)", slug: "time_queries", sortable: true, number: true},
|
||||
{label: "Queries", slug: "queries", sortable: true, number: true},
|
||||
{label: "Method", slug: "method"},
|
||||
{label: "Time", slug: "time", sortable: true},
|
||||
{label: __("Path"), slug: "path"},
|
||||
{label: __("Duration (ms)"), slug: "duration", sortable: true, number: true},
|
||||
{label: __("Time in Queries (ms)"), slug: "time_queries", sortable: true, number: true},
|
||||
{label: __("Queries"), slug: "queries", sortable: true, number: true},
|
||||
{label: __("Method"), slug: "method"},
|
||||
{label: __("Time"), slug: "time", sortable: true},
|
||||
],
|
||||
query: {
|
||||
sort: "duration",
|
||||
|
|
@ -140,7 +140,7 @@ export default {
|
|||
mounted() {
|
||||
this.fetch_status();
|
||||
this.refresh();
|
||||
this.$root.page.set_secondary_action("Clear", () => {
|
||||
this.$root.page.set_secondary_action(__("Clear"), () => {
|
||||
frappe.set_route("recorder");
|
||||
this.clear();
|
||||
});
|
||||
|
|
@ -151,11 +151,11 @@ export default {
|
|||
const current_page = this.query.pagination.page;
|
||||
const total_pages = this.query.pagination.total;
|
||||
return [{
|
||||
label: "First",
|
||||
label: __("First"),
|
||||
number: 1,
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
},{
|
||||
label: "Previous",
|
||||
label: __("Previous"),
|
||||
number: Math.max(current_page - 1, 1),
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
}, {
|
||||
|
|
@ -163,11 +163,11 @@ export default {
|
|||
number: current_page,
|
||||
status: "btn-info",
|
||||
}, {
|
||||
label: "Next",
|
||||
label: __("Next"),
|
||||
number: Math.min(current_page + 1, total_pages),
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}, {
|
||||
label: "Last",
|
||||
label: __("Last"),
|
||||
number: total_pages,
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}];
|
||||
|
|
@ -230,11 +230,11 @@ export default {
|
|||
},
|
||||
update_buttons: function() {
|
||||
if(this.status.status == "Active") {
|
||||
this.$root.page.set_primary_action("Stop", () => {
|
||||
this.$root.page.set_primary_action(__("Stop"), () => {
|
||||
this.stop();
|
||||
});
|
||||
} else {
|
||||
this.$root.page.set_primary_action("Start", () => {
|
||||
this.$root.page.set_primary_action(__("Start"), () => {
|
||||
this.start();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
</div>
|
||||
<div class="row form-section visible-section">
|
||||
<div class="col-sm-10">
|
||||
<h6 class="form-section-heading uppercase">SQL Queries</h6>
|
||||
<h6 class="form-section-heading uppercase">{{ __("SQL Queries") }}</h6>
|
||||
</div>
|
||||
<div class="col-sm-2 filter-list">
|
||||
<div class="sort-selector">
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
<div class="checkbox">
|
||||
<label>
|
||||
<span class="input-area"><input type="checkbox" class="input-with-feedback bold" data-fieldtype="Check" v-model="group_duplicates"></span>
|
||||
<span class="label-area small">Group Duplicate Queries</span>
|
||||
<span class="label-area small">{{ __("Group Duplicate Queries") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -48,15 +48,15 @@
|
|||
<div class="grid-row">
|
||||
<div class="data-row row">
|
||||
<div class="row-index col col-xs-1">
|
||||
<span>Index</span></div>
|
||||
<span>{{ __("Index") }}</span></div>
|
||||
<div class="col grid-static-col col-xs-6">
|
||||
<div class="static-area ellipsis">Query</div>
|
||||
<div class="static-area ellipsis">{{ __("Query") }}</div>
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2">
|
||||
<div class="static-area ellipsis text-right">Duration (ms)</div>
|
||||
<div class="static-area ellipsis text-right">{{ __("Duration (ms)") }}</div>
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2">
|
||||
<div class="static-area ellipsis text-right">Exact Copies</div>
|
||||
<div class="static-area ellipsis text-right">{{ __("Exact Copies") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -82,7 +82,7 @@
|
|||
<div class="recorder-form-in-grid" v-if="showing == call.index">
|
||||
<div class="grid-form-heading" @click="showing = null">
|
||||
<div class="toolbar grid-header-toolbar">
|
||||
<span class="panel-title">SQL Query #<span class="grid-form-row-index">{{ call.index }}</span></span>
|
||||
<span class="panel-title">{{ __("SQL Query") }} #<span class="grid-form-row-index">{{ call.index }}</span></span>
|
||||
<div class="btn btn-default btn-xs pull-right" style="margin-left: 7px;">
|
||||
<span class="hidden-xs octicon octicon-triangle-up"></span>
|
||||
</div>
|
||||
|
|
@ -98,25 +98,25 @@
|
|||
<form>
|
||||
<div class="frappe-control">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">Query</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ __("Query") }}</label></div>
|
||||
<div class="control-value like-disabled-input for-description"><pre>{{ call.query }}</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="frappe-control input-max-width">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">Duration (ms)</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ __("Duration (ms)") }}</label></div>
|
||||
<div class="control-value like-disabled-input">{{ call.duration }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="frappe-control input-max-width">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">Exact Copies</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ __("Exact Copies") }}</label></div>
|
||||
<div class="control-value like-disabled-input">{{ call.exact_copies }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="frappe-control">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">Stack Trace</label></div>
|
||||
<div class="clearfix"><label class="control-label"{{ __("Stack Trace") }}</label></div>
|
||||
<div class="control-value like-disabled-input for-description" style="overflow:auto">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
|
|
@ -137,7 +137,7 @@
|
|||
</div>
|
||||
<div class="frappe-control" v-if="call.explain_result[0]">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">SQL Explain</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ __("SQL Explain") }}</label></div>
|
||||
<div class="control-value like-disabled-input for-description" style="overflow:auto">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="request.calls.length == 0" class="grid-empty text-center">No Data</div>
|
||||
<div v-if="request.calls.length == 0" class="grid-empty text-center">{{ __("No Data") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -201,19 +201,19 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
columns: [
|
||||
{label: "Path", slug: "path", type: "Data", class: "col-sm-6"},
|
||||
{label: "CMD", slug: "cmd", type: "Data", class: "col-sm-6"},
|
||||
{label: "Time", slug: "time", type: "Time", class: "col-sm-6"},
|
||||
{label: "Duration (ms)", slug: "duration", type: "Float", class: "col-sm-6"},
|
||||
{label: "Number of Queries", slug: "queries", type: "Int", class: "col-sm-6"},
|
||||
{label: "Time in Queries (ms)", slug: "time_queries", type: "Float", class: "col-sm-6"},
|
||||
{label: "Request Headers", slug: "headers", type: "Small Text", formatter: value => `<pre class="for-description like-disabled-input">${JSON.stringify(value, null, 4)}</pre>`, class: "col-sm-12"},
|
||||
{label: "Form Dict", slug: "form_dict", type: "Small Text", formatter: value => `<pre class="for-description like-disabled-input">${JSON.stringify(value, null, 4)}</pre>`, class: "col-sm-12"},
|
||||
{label: __("Path"), slug: "path", type: "Data", class: "col-sm-6"},
|
||||
{label: __("CMD"), slug: "cmd", type: "Data", class: "col-sm-6"},
|
||||
{label: __("Time"), slug: "time", type: "Time", class: "col-sm-6"},
|
||||
{label: __("Duration (ms)"), slug: "duration", type: "Float", class: "col-sm-6"},
|
||||
{label: __("Number of Queries"), slug: "queries", type: "Int", class: "col-sm-6"},
|
||||
{label: __("Time in Queries (ms)"), slug: "time_queries", type: "Float", class: "col-sm-6"},
|
||||
{label: __("Request Headers"), slug: "headers", type: "Small Text", formatter: value => `<pre class="for-description like-disabled-input">${JSON.stringify(value, null, 4)}</pre>`, class: "col-sm-12"},
|
||||
{label: __("Form Dict"), slug: "form_dict", type: "Small Text", formatter: value => `<pre class="for-description like-disabled-input">${JSON.stringify(value, null, 4)}</pre>`, class: "col-sm-12"},
|
||||
],
|
||||
table_columns: [
|
||||
{label: "Execution Order", slug: "index", sortable: true},
|
||||
{label: "Duration (ms)", slug: "duration", sortable: true},
|
||||
{label: "Exact Copies", slug: "exact_copies", sortable: true},
|
||||
{label: __("Execution Order"), slug: "index", sortable: true},
|
||||
{label: __("Duration (ms)"), slug: "duration", sortable: true},
|
||||
{label: __("Exact Copies"), slug: "exact_copies", sortable: true},
|
||||
],
|
||||
query: {
|
||||
sort: "duration",
|
||||
|
|
@ -236,11 +236,11 @@ export default {
|
|||
const current_page = this.query.pagination.page;
|
||||
const total_pages = this.query.pagination.total;
|
||||
return [{
|
||||
label: "First",
|
||||
label: __("First"),
|
||||
number: 1,
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
},{
|
||||
label: "Previous",
|
||||
label: __("Previous"),
|
||||
number: Math.max(current_page - 1, 1),
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
}, {
|
||||
|
|
@ -248,11 +248,11 @@ export default {
|
|||
number: current_page,
|
||||
status: "btn-info",
|
||||
}, {
|
||||
label: "Next",
|
||||
label: __("Next"),
|
||||
number: Math.min(current_page + 1, total_pages),
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}, {
|
||||
label: "Last",
|
||||
label: __("Last"),
|
||||
number: total_pages,
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}];
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import RecorderRoot from "./RecorderRoot.vue";
|
|||
import RecorderDetail from "./RecorderDetail.vue";
|
||||
import RequestDetail from "./RequestDetail.vue";
|
||||
|
||||
Vue.prototype.__ = window.__;
|
||||
Vue.prototype.frappe = window.frappe;
|
||||
|
||||
Vue.use(VueRouter);
|
||||
const routes = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ class NotificationsView extends BaseNotificationsView {
|
|||
this.container.append($(`<div class="notification-null-state">
|
||||
<div class="text-center">
|
||||
<img src="/assets/frappe/images/ui-states/notification-empty-state.svg" alt="Generic Empty State" class="null-state">
|
||||
<div class="title">No New notifications</div>
|
||||
<div class="title">${__('No New notifications')}</div>
|
||||
<div class="subtitle">
|
||||
${__('Looks like you haven’t received any notifications.')}
|
||||
</div></div></div>`));
|
||||
|
|
@ -430,7 +430,7 @@ class EventsView extends BaseNotificationsView {
|
|||
<div class="notification-null-state">
|
||||
<div class="text-center">
|
||||
<img src="/assets/frappe/images/ui-states/event-empty-state.svg" alt="Generic Empty State" class="null-state">
|
||||
<div class="title">No Upcoming Events</div>
|
||||
<div class="title">${__('No Upcoming Events')}</div>
|
||||
<div class="subtitle">
|
||||
${__('There are no upcoming events for you.')}
|
||||
</div></div></div>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,34 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
|
|||
title: __("Switch Theme")
|
||||
});
|
||||
this.body = $(`<div class="theme-grid"></div>`).appendTo(this.dialog.$body);
|
||||
this.bind_events();
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
this.dialog.$wrapper.on('keydown', (e) => {
|
||||
if (!this.themes) return;
|
||||
|
||||
const key = frappe.ui.keys.get_key(e);
|
||||
let increment_by;
|
||||
|
||||
if (key === "right") {
|
||||
increment_by = 1;
|
||||
} else if (key === "left") {
|
||||
increment_by = -1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const current_index = this.themes.findIndex(theme => {
|
||||
return theme.name === this.current_theme;
|
||||
});
|
||||
|
||||
const new_theme = this.themes[current_index + increment_by];
|
||||
if (!new_theme) return;
|
||||
|
||||
new_theme.$html.click();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ window.validate_name = function(txt) {
|
|||
return frappe.utils.validate_type(txt, "name");
|
||||
};
|
||||
|
||||
window.validate_url = function(txt) {
|
||||
return frappe.utils.validate_type(txt, "url");
|
||||
};
|
||||
|
||||
window.nth = function(number) {
|
||||
number = cint(number);
|
||||
var s = 'th';
|
||||
|
|
|
|||
|
|
@ -405,7 +405,7 @@ Object.assign(frappe.utils, {
|
|||
regExp = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
break;
|
||||
case "url":
|
||||
regExp = /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!$&'()*+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[$&'()*+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!$&'()*+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!$&'()*+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!$&'()*+,;=]|:|@)|\/|\?)*)?$/i;
|
||||
regExp = /^((([A-Za-z0-9.+-]+:(?:\/\/)?)(?:[-;:&=\+\,\w]@)?[A-Za-z0-9.-]+(:[0-9]+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/i;
|
||||
break;
|
||||
case "dateIso":
|
||||
regExp = /^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$/;
|
||||
|
|
@ -831,10 +831,13 @@ Object.assign(frappe.utils, {
|
|||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
},
|
||||
get_form_link: function(doctype, name, html = false, display_text = null) {
|
||||
get_form_link: function(doctype, name, html=false, display_text=null, query_params_obj=null) {
|
||||
display_text = display_text || name;
|
||||
name = encodeURIComponent(name);
|
||||
const route = `/app/${encodeURIComponent(doctype.toLowerCase().replace(/ /g, '-'))}/${name}`;
|
||||
let route = `/app/${encodeURIComponent(doctype.toLowerCase().replace(/ /g, '-'))}/${name}`;
|
||||
if (query_params_obj) {
|
||||
route += frappe.utils.make_query_string(query_params_obj);
|
||||
}
|
||||
if (html) {
|
||||
return `<a href="${route}">${display_text}</a>`;
|
||||
}
|
||||
|
|
@ -1286,31 +1289,6 @@ Object.assign(frappe.utils, {
|
|||
</div>`);
|
||||
},
|
||||
|
||||
get_names_for_mentions() {
|
||||
let names_for_mentions = Object.keys(frappe.boot.user_info || [])
|
||||
.filter(user => {
|
||||
return !["Administrator", "Guest"].includes(user)
|
||||
&& frappe.boot.user_info[user].allowed_in_mentions
|
||||
&& frappe.boot.user_info[user].user_type === 'System User';
|
||||
})
|
||||
.map(user => {
|
||||
return {
|
||||
id: frappe.boot.user_info[user].name,
|
||||
value: frappe.boot.user_info[user].fullname,
|
||||
};
|
||||
});
|
||||
|
||||
frappe.boot.user_groups && frappe.boot.user_groups.map(group => {
|
||||
names_for_mentions.push({
|
||||
id: group,
|
||||
value: group,
|
||||
is_group: true,
|
||||
link: frappe.utils.get_form_link('User Group', group)
|
||||
});
|
||||
});
|
||||
|
||||
return names_for_mentions;
|
||||
},
|
||||
print(doctype, docname, print_format, letterhead, lang_code) {
|
||||
let w = window.open(
|
||||
frappe.urllib.get_full_url(
|
||||
|
|
@ -1333,5 +1311,11 @@ Object.assign(frappe.utils, {
|
|||
frappe.msgprint(__('Please enable pop-ups'));
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
get_clipboard_data(clipboard_paste_event) {
|
||||
let e = clipboard_paste_event;
|
||||
let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
|
||||
return clipboard_data.getData('Text');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ frappe.breadcrumbs = {
|
|||
if (breadcrumbs.doctype && ["print", "form"].includes(view)) {
|
||||
this.set_list_breadcrumb(breadcrumbs);
|
||||
this.set_form_breadcrumb(breadcrumbs, view);
|
||||
} else if (breadcrumbs.doctype && view === 'list') {
|
||||
this.set_list_breadcrumb(breadcrumbs);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
|
|||
setup_defaults() {
|
||||
return super.setup_defaults()
|
||||
.then(() => {
|
||||
this.page_title = __('{0} Dashboard', [this.doctype]);
|
||||
this.page_title = __('{0} Dashboard', [__(this.doctype)]);
|
||||
this.dashboard_settings = frappe.get_user_settings(this.doctype)['dashboard_settings'] || null;
|
||||
});
|
||||
}
|
||||
|
|
@ -271,7 +271,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
|
|||
show_add_chart_dialog() {
|
||||
let fields = this.get_field_options();
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Add a {0} Chart", [this.doctype]),
|
||||
title: __("Add a {0} Chart", [__(this.doctype)]),
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'new_or_existing',
|
||||
|
|
|
|||
|
|
@ -335,12 +335,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
let message;
|
||||
if (dashboard_name) {
|
||||
let dashboard_route_html = `<a href="#dashboard-view/${dashboard_name}">${dashboard_name}</a>`;
|
||||
message = __("New {0} {1} added to Dashboard {2}", [doctype, name, dashboard_route_html]);
|
||||
message = __("New {0} {1} added to Dashboard {2}", [__(doctype), name, dashboard_route_html]);
|
||||
} else {
|
||||
message = __("New {0} {1} created", [doctype, name]);
|
||||
message = __("New {0} {1} created", [__(doctype), name]);
|
||||
}
|
||||
|
||||
frappe.msgprint(message, __("New {0} Created", [doctype]));
|
||||
frappe.msgprint(message, __("New {0} Created", [__(doctype)]));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -937,7 +937,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
else {
|
||||
wrapper[0].innerHTML =
|
||||
`<div class="flex justify-center align-center text-muted" style="height: 120px; display: flex;">
|
||||
<div>Please select X and Y fields</div>
|
||||
<div>${__("Please select X and Y fields")}</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1094,7 +1094,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
|
||||
return Object.assign(column, {
|
||||
id: column.fieldname,
|
||||
name: __(column.label),
|
||||
name: __(column.label, null, `Column of report '${this.report_name}'`), // context has to match context in get_messages_from_report in translate.py
|
||||
width: parseInt(column.width) || null,
|
||||
editable: false,
|
||||
compareValue: compareFn,
|
||||
|
|
@ -1343,7 +1343,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
|
||||
open_url_post(frappe.request.url, args);
|
||||
}
|
||||
}, __('Export Report: '+ this.report_name), __('Download'));
|
||||
}, __('Export Report: {0}', [this.report_name]), __('Download'));
|
||||
}
|
||||
|
||||
get_data_for_csv(include_indentation) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import Widget from "./base_widget.js";
|
|||
|
||||
frappe.provide("frappe.utils");
|
||||
|
||||
const INDICATOR_COLORS = ["Grey", "Green", "Red", "Orange", "Pink", "Yellow", "Blue", "Cyan", "Teal"];
|
||||
export default class ShortcutWidget extends Widget {
|
||||
constructor(opts) {
|
||||
opts.shadow = true;
|
||||
|
|
@ -79,7 +78,7 @@ export default class ShortcutWidget extends Widget {
|
|||
|
||||
this.action_area.empty();
|
||||
const label = get_label();
|
||||
let color = INDICATOR_COLORS.includes(this.color) && count ? this.color.toLowerCase() : 'gray';
|
||||
let color = this.color && count ? this.color.toLowerCase() : 'gray';
|
||||
$(`<div class="indicator-pill ellipsis ${color}">${label}</div>`).appendTo(this.action_area);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,9 +237,19 @@ class ShortcutDialog extends WidgetDialog {
|
|||
hidden: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Color",
|
||||
fieldtype: "Select",
|
||||
fieldname: "color",
|
||||
label: __("Color"),
|
||||
options: ["Grey", "Green", "Red", "Orange", "Pink", "Yellow", "Blue", "Cyan"],
|
||||
default: "Grey",
|
||||
onchange: () => {
|
||||
let color = this.dialog.fields_dict.color.value.toLowerCase();
|
||||
let $select = this.dialog.fields_dict.color.$input;
|
||||
if (!$select.parent().find('.color-box').get(0)) {
|
||||
$(`<div class="color-box"></div>`).insertBefore($select.get(0));
|
||||
}
|
||||
$select.parent().find('.color-box').get(0).style.backgroundColor = `var(--text-on-${color})`;
|
||||
}
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
|
|
|
|||
|
|
@ -121,3 +121,10 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-row.row {
|
||||
.selected-color {
|
||||
top: calc(50% - 11px);
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,17 @@
|
|||
--blue-100: #D3E9FC;
|
||||
--blue-50 : #F0F8FE;
|
||||
|
||||
--cyan-900: #006464;
|
||||
--cyan-800: #007272;
|
||||
--cyan-700: #008b8b;
|
||||
--cyan-600: #02c5c5;
|
||||
--cyan-500: #00ffff;
|
||||
--cyan-400: #2ef8f8;
|
||||
--cyan-300: #6efcfc;
|
||||
--cyan-200: #a0f8f8;
|
||||
--cyan-100: #c7fcfc;
|
||||
--cyan-50 : #dafafa;
|
||||
|
||||
--green-900: #2D401D;
|
||||
--green-800: #44622A;
|
||||
--green-700: #518B21;
|
||||
|
|
@ -151,6 +162,8 @@
|
|||
--bg-gray: var(--gray-200);
|
||||
--bg-light-gray: var(--gray-100);
|
||||
--bg-purple: var(--purple-100);
|
||||
--bg-pink: var(--pink-50);
|
||||
--bg-cyan: var(--cyan-50);
|
||||
|
||||
--text-on-blue: var(--blue-600);
|
||||
--text-on-light-blue: var(--blue-500);
|
||||
|
|
@ -163,6 +176,8 @@
|
|||
--text-on-gray: var(--gray-600);
|
||||
--text-on-light-gray: var(--gray-800);
|
||||
--text-on-purple: var(--purple-500);
|
||||
--text-on-pink: var(--pink-500);
|
||||
--text-on-cyan: var(--cyan-600);
|
||||
|
||||
--awesomplete-hover-bg: var(--control-bg);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,73 +16,82 @@
|
|||
}
|
||||
}
|
||||
|
||||
$check-icon: url("data:image/svg+xml, <svg viewBox='0 0 8 7' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M1 4.00001L2.66667 5.80001L7 1.20001' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
||||
|
||||
input[type="checkbox"] {
|
||||
position: relative;
|
||||
width: 0 !important;
|
||||
height: var(--custom-checkbox-size);
|
||||
margin-right: calc(var(--custom-checkbox-size) + var(--checkbox-right-margin)) !important;
|
||||
font-size: calc(var(--custom-checkbox-size) - 1px);
|
||||
width: var(--checkbox-size) !important;
|
||||
height: var(--checkbox-size);
|
||||
margin-right: var(--checkbox-right-margin) !important;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
border: 1px solid var(--gray-400);
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
|
||||
&:before {
|
||||
width: var(--custom-checkbox-size);
|
||||
height: var(--custom-checkbox-size);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
content: ' ';
|
||||
border: 1px solid var(--gray-400);
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
// Reset browser behavior
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
-webkit-print-color-adjust: exact;
|
||||
color-adjust: exact;
|
||||
|
||||
.grid-static-col & {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
&:checked:before {
|
||||
content: url("data: image/svg+xml;utf8, <svg width='8' height='7' viewBox='0 0 8 7' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M1 4.00001L2.66667 5.80001L7 1.20001' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
||||
background: linear-gradient(180deg, #4AC3F8 -124.51%, #2490EF 100%);
|
||||
&:checked {
|
||||
background-color: #2490EF;
|
||||
background-image: $check-icon, linear-gradient(180deg, #4AC3F8 -124.51%, #2490EF 100%);
|
||||
background-size: 57%, 100%;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none; // Prevent browser behavior
|
||||
box-shadow: var(--checkbox-focus-shadow);
|
||||
}
|
||||
|
||||
&.disabled-deselected:before, &:disabled:not([checked])::before {
|
||||
background: var(--disabled-control-bg);
|
||||
border: 0.5px solid var(--gray-300);
|
||||
box-sizing: border-box;
|
||||
&.disabled-deselected, &:disabled {
|
||||
background-color: var(--disabled-control-bg);
|
||||
box-shadow: inset 0px 1px 7px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
border: 0.5px solid var(--gray-300);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.disabled-selected:before, &:disabled:checked::before {
|
||||
content: url("data: image/svg+xml;utf8, <svg width='8' height='7' viewBox='0 0 8 7' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M1 4.00001L2.66667 5.80001L7 1.20001' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
||||
background: var(--gray-500);
|
||||
box-sizing: border-box;
|
||||
&.disabled-selected, &:disabled:checked {
|
||||
background-color: var(--gray-500);
|
||||
background-image: $check-icon;
|
||||
background-size: 57%;
|
||||
box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
line-height: 10px;
|
||||
border: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Firefox doesn't support
|
||||
// pseudo elements on checkbox
|
||||
html.firefox, html.safari {
|
||||
:root {
|
||||
--custom-checkbox-size: 0px;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
width: var(--base-checkbox-size) !important;
|
||||
height: var(--base-checkbox-size);
|
||||
margin-right: var(--checkbox-right-margin) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.frappe-card {
|
||||
@include card();
|
||||
}
|
||||
|
||||
.frappe-control[data-fieldtype="Select"].frappe-control[data-fieldname="color"] {
|
||||
select {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.color-box {
|
||||
position: absolute;
|
||||
top: calc(50% - 11px);
|
||||
left: 8px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 5px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.frappe-control[data-fieldtype="Select"] .control-input,
|
||||
.frappe-control[data-fieldtype="Select"].form-group {
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@
|
|||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.grid-static-col[data-fieldtype="Code"] {
|
||||
.grid-static-col[data-fieldtype="Code"], .grid-static-col[data-fieldtype="HTML Editor"] {
|
||||
overflow: hidden;
|
||||
|
||||
.static-area {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,16 @@
|
|||
@include indicator-pill-color('green');
|
||||
}
|
||||
|
||||
.indicator.cyan {
|
||||
@include indicator-color('cyan');
|
||||
}
|
||||
|
||||
.indicator-pill.cyan,
|
||||
.indicator-pill-right.cyan,
|
||||
.indicator-pill-round.cyan {
|
||||
@include indicator-pill-color('cyan');
|
||||
}
|
||||
|
||||
.indicator.blue {
|
||||
@include indicator-color('blue');
|
||||
}
|
||||
|
|
@ -131,6 +141,16 @@
|
|||
@include indicator-pill-color('red');
|
||||
}
|
||||
|
||||
.indicator.pink {
|
||||
@include indicator-color('pink');
|
||||
}
|
||||
|
||||
.indicator-pill.pink,
|
||||
.indicator-pill-right.pink,
|
||||
.indicator-pill-round.pink {
|
||||
@include indicator-pill-color('pink');
|
||||
}
|
||||
|
||||
.indicator-pill.darkgrey,
|
||||
.indicator-pill-right.darkgrey,
|
||||
.indicator-pill-round.darkgrey {
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@
|
|||
padding: 10px 12px;
|
||||
height: initial;
|
||||
line-height: initial;
|
||||
cursor: pointer;
|
||||
|
||||
&.selected {
|
||||
background-color: var(--control-bg);
|
||||
|
|
@ -196,5 +197,8 @@
|
|||
}
|
||||
|
||||
.mention[data-is-group="true"] {
|
||||
background-color: var(--group-mention-bg-color);
|
||||
.icon {
|
||||
margin-top: -2px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,9 +31,6 @@ $input-height: 28px !default;
|
|||
--modal-bg: white;
|
||||
--toast-bg: var(--modal-bg);
|
||||
--popover-bg: white;
|
||||
--checkbox-right-margin: var(--margin-xs);
|
||||
--base-checkbox-size: 14px;
|
||||
--custom-checkbox-size: 14px;
|
||||
|
||||
--appreciation-color: var(--dark-green-600);
|
||||
--appreciation-bg: var(--dark-green-100);
|
||||
|
|
@ -52,6 +49,11 @@ $input-height: 28px !default;
|
|||
--input-height: #{$input-height};
|
||||
--input-disabled-bg: var(--gray-200);
|
||||
|
||||
// checkbox
|
||||
--checkbox-right-margin: var(--margin-xs);
|
||||
--checkbox-size: 14px;
|
||||
--checkbox-focus-shadow: 0 0 0 2px var(--gray-300);
|
||||
|
||||
// timeline
|
||||
--timeline-item-icon-size: 34px;
|
||||
--timeline-item-left-margin: var(--margin-xl);
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@
|
|||
// input
|
||||
--input-disabled-bg: none;
|
||||
|
||||
// checkbox
|
||||
--checkbox-focus-shadow: 0 0 0 2px var(--gray-600);
|
||||
|
||||
color-scheme: dark;
|
||||
|
||||
.frappe-card {
|
||||
|
|
|
|||
|
|
@ -197,8 +197,7 @@ $level-margin-right: 8px;
|
|||
|
||||
input.list-check-all, input.list-row-checkbox {
|
||||
margin-top: 0px;
|
||||
margin-left: calc(var(--custom-checkbox-size) / 2);
|
||||
--checkbox-right-margin: #{$level-margin-right};
|
||||
--checkbox-right-margin: calc(var(--checkbox-size) / 2 + #{$level-margin-right});
|
||||
}
|
||||
|
||||
.filterable {
|
||||
|
|
|
|||
|
|
@ -365,6 +365,17 @@ class TestCommands(BaseTestCommands):
|
|||
installed_apps = set(frappe.get_installed_apps())
|
||||
self.assertSetEqual(list_apps, installed_apps)
|
||||
|
||||
# test 3: parse json format
|
||||
self.execute("bench --site all list-apps --format json")
|
||||
self.assertEquals(self.returncode, 0)
|
||||
self.assertIsInstance(json.loads(self.stdout), dict)
|
||||
|
||||
self.execute("bench --site {site} list-apps --format json")
|
||||
self.assertIsInstance(json.loads(self.stdout), dict)
|
||||
|
||||
self.execute("bench --site {site} list-apps -f json")
|
||||
self.assertIsInstance(json.loads(self.stdout), dict)
|
||||
|
||||
def test_get_bench_relative_path(self):
|
||||
bench_path = frappe.utils.get_bench_path()
|
||||
test1_path = os.path.join(bench_path, "test1.txt")
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import unittest
|
||||
import frappe
|
||||
|
||||
from frappe.utils import evaluate_filters, money_in_words, scrub_urls, get_url
|
||||
from frappe.utils import validate_url, validate_email_address
|
||||
from frappe.utils import ceil, floor
|
||||
|
||||
from PIL import Image
|
||||
|
|
@ -54,14 +56,15 @@ class TestMoney(unittest.TestCase):
|
|||
|
||||
for num in nums_bhd:
|
||||
self.assertEqual(
|
||||
money_in_words(num[0], "BHD"), num[1], "{0} is not the same as {1}".
|
||||
format(money_in_words(num[0], "BHD"), num[1])
|
||||
money_in_words(num[0], "BHD"),
|
||||
num[1],
|
||||
"{0} is not the same as {1}".format(money_in_words(num[0], "BHD"), num[1])
|
||||
)
|
||||
|
||||
for num in nums_ngn:
|
||||
self.assertEqual(
|
||||
money_in_words(num[0], "NGN"), num[1], "{0} is not the same as {1}".
|
||||
format(money_in_words(num[0], "NGN"), num[1])
|
||||
money_in_words(num[0], "NGN"), num[1],
|
||||
"{0} is not the same as {1}".format(money_in_words(num[0], "NGN"), num[1])
|
||||
)
|
||||
|
||||
class TestDataManipulation(unittest.TestCase):
|
||||
|
|
@ -93,7 +96,7 @@ class TestDataManipulation(unittest.TestCase):
|
|||
class TestMathUtils(unittest.TestCase):
|
||||
def test_floor(self):
|
||||
from decimal import Decimal
|
||||
self.assertEqual(floor(2), 2 )
|
||||
self.assertEqual(floor(2), 2)
|
||||
self.assertEqual(floor(12.32904), 12)
|
||||
self.assertEqual(floor(22.7330), 22)
|
||||
self.assertEqual(floor('24.7'), 24)
|
||||
|
|
@ -102,7 +105,7 @@ class TestMathUtils(unittest.TestCase):
|
|||
|
||||
def test_ceil(self):
|
||||
from decimal import Decimal
|
||||
self.assertEqual(ceil(2), 2 )
|
||||
self.assertEqual(ceil(2), 2)
|
||||
self.assertEqual(ceil(12.32904), 13)
|
||||
self.assertEqual(ceil(22.7330), 23)
|
||||
self.assertEqual(ceil('24.7'), 25)
|
||||
|
|
@ -127,6 +130,56 @@ class TestHTMLUtils(unittest.TestCase):
|
|||
self.assertTrue('<h1>Hello</h1>' in clean)
|
||||
self.assertTrue('<a href="http://test.com">text</a>' in clean)
|
||||
|
||||
class TestValidationUtils(unittest.TestCase):
|
||||
def test_valid_url(self):
|
||||
# Edge cases
|
||||
self.assertFalse(validate_url(''))
|
||||
self.assertFalse(validate_url(None))
|
||||
|
||||
# Valid URLs
|
||||
self.assertTrue(validate_url('https://google.com'))
|
||||
self.assertTrue(validate_url('http://frappe.io', throw=True))
|
||||
|
||||
# Invalid URLs without throw
|
||||
self.assertFalse(validate_url('google.io'))
|
||||
self.assertFalse(validate_url('google.io'))
|
||||
|
||||
# Invalid URL with throw
|
||||
self.assertRaises(frappe.ValidationError, validate_url, 'frappe', throw=True)
|
||||
|
||||
# Scheme validation
|
||||
self.assertFalse(validate_url('https://google.com', valid_schemes='http'))
|
||||
self.assertTrue(validate_url('ftp://frappe.cloud', valid_schemes=['https', 'ftp']))
|
||||
self.assertFalse(validate_url('bolo://frappe.io', valid_schemes=("http", "https", "ftp", "ftps")))
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
validate_url,
|
||||
'gopher://frappe.io',
|
||||
valid_schemes='https',
|
||||
throw=True
|
||||
)
|
||||
|
||||
def test_valid_email(self):
|
||||
# Edge cases
|
||||
self.assertFalse(validate_email_address(''))
|
||||
self.assertFalse(validate_email_address(None))
|
||||
|
||||
# Valid addresses
|
||||
self.assertTrue(validate_email_address('someone@frappe.com'))
|
||||
self.assertTrue(validate_email_address('someone@frappe.com, anyone@frappe.io'))
|
||||
|
||||
# Invalid address
|
||||
self.assertFalse(validate_email_address('someone'))
|
||||
self.assertFalse(validate_email_address('someone@----.com'))
|
||||
|
||||
# Invalid with throw
|
||||
self.assertRaises(
|
||||
frappe.InvalidEmailAddressError,
|
||||
validate_email_address,
|
||||
'someone.com',
|
||||
throw=True
|
||||
)
|
||||
|
||||
class TestImage(unittest.TestCase):
|
||||
def test_strip_exif_data(self):
|
||||
original_image = Image.open("../apps/frappe/frappe/tests/data/exif_sample_image.jpg")
|
||||
|
|
|
|||
|
|
@ -443,8 +443,16 @@ def get_messages_from_report(name):
|
|||
messages = _get_messages_from_page_or_report("Report", name,
|
||||
frappe.db.get_value("DocType", report.ref_doctype, "module"))
|
||||
|
||||
if report.columns:
|
||||
context = "Column of report '%s'" % report.name # context has to match context in `prepare_columns` in query_report.js
|
||||
messages.extend([(None, report_column.label, context) for report_column in report.columns])
|
||||
|
||||
if report.filters:
|
||||
messages.extend([(None, report_filter.label) for report_filter in report.filters])
|
||||
|
||||
if report.query:
|
||||
messages.extend([(None, message) for message in re.findall('"([^:,^"]*):', report.query) if is_translatable(message)])
|
||||
|
||||
messages.append((None,report.report_name))
|
||||
return messages
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ from email.utils import formataddr, parseaddr
|
|||
from gzip import GzipFile
|
||||
from typing import Generator, Iterable
|
||||
from urllib.parse import quote, urlparse
|
||||
|
||||
from werkzeug.test import Client
|
||||
|
||||
import frappe
|
||||
|
|
@ -156,6 +155,33 @@ def split_emails(txt):
|
|||
|
||||
return email_list
|
||||
|
||||
def validate_url(txt, throw=False, valid_schemes=None):
|
||||
"""
|
||||
Checks whether `txt` has a valid URL string
|
||||
|
||||
Parameters:
|
||||
throw (`bool`): throws a validationError if URL is not valid
|
||||
valid_schemes (`str` or `list`): if provided checks the given URL's scheme against this
|
||||
|
||||
Returns:
|
||||
bool: if `txt` represents a valid URL
|
||||
"""
|
||||
url = urlparse(txt)
|
||||
is_valid = bool(url.netloc)
|
||||
|
||||
# Handle scheme validation
|
||||
if isinstance(valid_schemes, str):
|
||||
is_valid = is_valid and (url.scheme == valid_schemes)
|
||||
elif isinstance(valid_schemes, (list, tuple, set)):
|
||||
is_valid = is_valid and (url.scheme in valid_schemes)
|
||||
|
||||
if not is_valid and throw:
|
||||
frappe.throw(
|
||||
frappe._("'{0}' is not a valid URL").format(frappe.bold(txt))
|
||||
)
|
||||
|
||||
return is_valid
|
||||
|
||||
def random_string(length):
|
||||
"""generate a random string"""
|
||||
import string
|
||||
|
|
@ -821,11 +847,3 @@ def groupby_metric(iterable: typing.Dict[str, list], key: str):
|
|||
for item in items:
|
||||
records.setdefault(item[key], {}).setdefault(category, []).append(item)
|
||||
return records
|
||||
|
||||
|
||||
def validate_url(url_string):
|
||||
try:
|
||||
result = urlparse(url_string)
|
||||
return result.scheme and result.scheme in ["http", "https", "ftp", "ftps"]
|
||||
except Exception:
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@
|
|||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"email",
|
||||
"status"
|
||||
"status",
|
||||
"anonymization_matrix",
|
||||
"deletion_steps"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -27,10 +29,23 @@
|
|||
"label": "Status",
|
||||
"options": "Pending Verification\nPending Approval\nDeleted",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "anonymization_matrix",
|
||||
"fieldtype": "Code",
|
||||
"label": "Anonymization Matrix",
|
||||
"options": "JSON",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "deletion_steps",
|
||||
"fieldtype": "Table",
|
||||
"label": "Deletion Steps ",
|
||||
"options": "Personal Data Deletion Step"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-02-28 12:36:08.219719",
|
||||
"modified": "2021-04-23 13:25:53.629308",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Personal Data Deletion Request",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ from frappe.model.document import Document
|
|||
from frappe.utils import get_fullname
|
||||
from frappe.utils.user import get_system_managers
|
||||
from frappe.utils.verified_command import get_signed_params, verify_request
|
||||
import json
|
||||
from frappe.core.utils import find
|
||||
|
||||
|
||||
class PersonalDataDeletionRequest(Document):
|
||||
|
|
@ -118,6 +120,24 @@ class PersonalDataDeletionRequest(Document):
|
|||
now=frappe.flags.in_test,
|
||||
)
|
||||
|
||||
def add_deletion_steps(self):
|
||||
if self.deletion_steps:
|
||||
return
|
||||
|
||||
for step in self.full_match_privacy_docs + self.partial_privacy_docs:
|
||||
row_data = {
|
||||
"status": "Pending",
|
||||
"document_type": step.get("doctype"),
|
||||
"partial": step.get("partial") or False,
|
||||
"fields": json.dumps(step.get("redact_fields", [])),
|
||||
"filtered_by": step.get("filtered_by") or "",
|
||||
}
|
||||
self.append("deletion_steps", row_data)
|
||||
|
||||
self.anonymization_matrix = json.dumps(self.anonymization_value_map, indent=4)
|
||||
self.save()
|
||||
self.reload()
|
||||
|
||||
def redact_partial_match_data(self, doctype):
|
||||
self.__redact_partial_match_data(doctype)
|
||||
self.rename_documents(doctype)
|
||||
|
|
@ -207,21 +227,57 @@ class PersonalDataDeletionRequest(Document):
|
|||
ref["doctype"], doc["name"], self.anon, force=True, show_alert=False
|
||||
)
|
||||
|
||||
def _anonymize_data(self, email=None, anon=None, set_data=True):
|
||||
def _anonymize_data(self, email=None, anon=None, set_data=True, commit=False):
|
||||
email = email or self.email
|
||||
anon = anon or self.name
|
||||
|
||||
if set_data:
|
||||
self.__set_anonymization_data(email, anon)
|
||||
|
||||
for doctype in self.full_match_privacy_docs:
|
||||
self.redact_full_match_data(doctype, email)
|
||||
self.add_deletion_steps()
|
||||
|
||||
for doctype in self.partial_privacy_docs:
|
||||
self.full_match_doctypes = (
|
||||
x
|
||||
for x in self.full_match_privacy_docs
|
||||
if filter(
|
||||
lambda x: x.document_type == x and x.status == "Pending", self.deletion_steps
|
||||
)
|
||||
)
|
||||
|
||||
self.partial_match_doctypes = (
|
||||
x
|
||||
for x in self.partial_privacy_docs
|
||||
if filter(
|
||||
lambda x: x.document_type == x and x.status == "Pending", self.deletion_steps
|
||||
)
|
||||
)
|
||||
|
||||
for doctype in self.full_match_doctypes:
|
||||
self.redact_full_match_data(doctype, email)
|
||||
self.set_step_status(doctype["doctype"])
|
||||
if commit:
|
||||
frappe.db.commit()
|
||||
|
||||
for doctype in self.partial_match_doctypes:
|
||||
self.redact_partial_match_data(doctype)
|
||||
self.set_step_status(doctype["doctype"])
|
||||
if commit:
|
||||
frappe.db.commit()
|
||||
|
||||
frappe.rename_doc("User", email, anon, force=True, show_alert=False)
|
||||
self.db_set("status", "Deleted")
|
||||
if commit:
|
||||
frappe.db.commit()
|
||||
|
||||
def set_step_status(self, step, status="Deleted"):
|
||||
del_step = find(self.deletion_steps, lambda x: x.document_type == step and x.status != status)
|
||||
|
||||
if not del_step:
|
||||
del_step = find(self.deletion_steps, lambda x: x.document_type == step)
|
||||
|
||||
del_step.status = status
|
||||
self.save()
|
||||
self.reload()
|
||||
|
||||
def __set_anonymization_data(self, email, anon):
|
||||
self.anon = anon or self.name
|
||||
|
|
@ -290,9 +346,8 @@ def confirm_deletion(email, name, host_name):
|
|||
frappe.db.commit()
|
||||
frappe.respond_as_web_page(
|
||||
_("Confirmed"),
|
||||
_(
|
||||
"The process for deletion of {0} data associated with {1} has been initiated."
|
||||
).format(host_name, email),
|
||||
_("The process for deletion of {0} data associated with {1} has been initiated.")
|
||||
.format(host_name, email),
|
||||
indicator_color="green",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2021-04-23 13:25:26.162797",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"document_type",
|
||||
"status",
|
||||
"partial",
|
||||
"fields",
|
||||
"filtered_by"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "document_type",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Document Type",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_preview": 1,
|
||||
"label": "Status",
|
||||
"options": "Pending\nDeleted",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "partial",
|
||||
"fieldtype": "Check",
|
||||
"in_preview": 1,
|
||||
"label": "Partial",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "fields",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Fields",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "filtered_by",
|
||||
"fieldtype": "Data",
|
||||
"label": "Filtered By",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-23 13:48:59.658681",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Personal Data Deletion Step",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class PersonalDataDeletionStep(Document):
|
||||
pass
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Fieldtype",
|
||||
"options": "Attach\nAttach Image\nCheck\nCurrency\nData\nDate\nDatetime\nDuration\nFloat\nHTML\nInt\nLink\nRating\nSelect\nSmall Text\nText\nText Editor\nTable\nSection Break\nColumn Break"
|
||||
"options": "Attach\nAttach Image\nCheck\nCurrency\nData\nDate\nDatetime\nDuration\nFloat\nHTML\nInt\nLink\nPassword\nRating\nSelect\nSmall Text\nText\nText Editor\nTable\nSection Break\nColumn Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "label",
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-10 23:20:44.354862",
|
||||
"modified": "2021-04-30 12:02:25.422345",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Web Form Field",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@
|
|||
{%- endif -%}
|
||||
<div class="split-section-content col-12 {{ left_col if image_on_right else right_col }} {{ align_content }}">
|
||||
<h2>{{ title }}</h2>
|
||||
{%- if content -%}
|
||||
<p>{{ content }}</p>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if link_label and link_url -%}
|
||||
<a href="{{ link_url }}">{{ link_label }}</a>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue