Merge branch 'develop' of https://github.com/frappe/frappe into double-signature-in-email
This commit is contained in:
commit
ca64d50dd4
83 changed files with 707 additions and 409 deletions
22
.github/workflows/semgrep.yml
vendored
Normal file
22
.github/workflows/semgrep.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
name: Semgrep
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
jobs:
|
||||
semgrep:
|
||||
name: Frappe Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup python3
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Run semgrep
|
||||
run: |
|
||||
python -m pip install -q semgrep
|
||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||
if [ -f .semgrep.yml ]; then semgrep --config=.semgrep.yml --quiet --error $files; fi
|
||||
29
.semgrep.yml
Normal file
29
.semgrep.yml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#Reference: https://semgrep.dev/docs/writing-rules/rule-syntax/
|
||||
|
||||
rules:
|
||||
- id: eval
|
||||
patterns:
|
||||
- pattern-not: eval("...")
|
||||
- pattern: eval(...)
|
||||
message: |
|
||||
Detected the use of eval(). eval() can be dangerous if used to evaluate
|
||||
dynamic content. Avoid it or use safe_eval().
|
||||
languages:
|
||||
- python
|
||||
severity: ERROR
|
||||
|
||||
# translations
|
||||
- id: frappe-translation-syntax-python
|
||||
pattern-either:
|
||||
- pattern: _(f"...") # f-strings not allowed
|
||||
- pattern: _("..." + "...") # concatenation not allowed
|
||||
- pattern: _("") # empty string is meaningless
|
||||
- pattern: _("..." % ...) # Only positional formatters are allowed.
|
||||
- pattern: _("...".format(...)) # format should not be used before translating
|
||||
- pattern: _("...") + ... + _("...") # don't split strings
|
||||
message: |
|
||||
Incorrect use of translation function detected.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages:
|
||||
- python
|
||||
severity: ERROR
|
||||
|
|
@ -7,7 +7,7 @@ addons:
|
|||
- test_site_producer
|
||||
mariadb: 10.3
|
||||
postgresql: 9.5
|
||||
chrome: stable
|
||||
firefox: latest
|
||||
|
||||
services:
|
||||
- xvfb
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ website/ @prssanna
|
|||
web_form/ @prssanna
|
||||
templates/ @surajshetty3416
|
||||
www/ @surajshetty3416
|
||||
integrations/ @nextchamp-saqib
|
||||
integrations/ @leela
|
||||
patches/ @surajshetty3416
|
||||
dashboard/ @prssanna
|
||||
email/ @saurabh6790
|
||||
email/ @leela
|
||||
event_streaming/ @ruchamahabal
|
||||
data_import* @netchampfaris
|
||||
core/ @surajshetty3416
|
||||
|
|
|
|||
|
|
@ -3,6 +3,16 @@ context('Recorder', () => {
|
|||
cy.login();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/app/recorder');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
// reset recorder
|
||||
return frappe.xcall("frappe.recorder.stop").then(() => {
|
||||
return frappe.xcall("frappe.recorder.delete");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Navigate to Recorder', () => {
|
||||
cy.visit('/app');
|
||||
cy.awesomebar('recorder');
|
||||
|
|
@ -11,7 +21,6 @@ context('Recorder', () => {
|
|||
});
|
||||
|
||||
it('Recorder Empty State', () => {
|
||||
cy.visit('/app/recorder');
|
||||
cy.get('.title-text').should('contain', 'Recorder');
|
||||
|
||||
cy.get('.indicator-pill').should('contain', 'Inactive').should('have.class', 'red');
|
||||
|
|
@ -24,7 +33,6 @@ context('Recorder', () => {
|
|||
});
|
||||
|
||||
it('Recorder Start', () => {
|
||||
cy.visit('/app/recorder');
|
||||
cy.get('.primary-action').should('contain', 'Start').click();
|
||||
cy.get('.indicator-pill').should('contain', 'Active').should('have.class', 'green');
|
||||
|
||||
|
|
@ -40,15 +48,9 @@ context('Recorder', () => {
|
|||
cy.visit('/app/recorder');
|
||||
cy.get('.title-text').should('contain', 'Recorder');
|
||||
cy.get('.result-list').should('contain', '/api/method/frappe.desk.reportview.get');
|
||||
|
||||
cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
|
||||
cy.wait(500);
|
||||
cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
|
||||
cy.get('.msg-box').should('contain', 'Inactive');
|
||||
});
|
||||
|
||||
it('Recorder View Request', () => {
|
||||
cy.visit('/app/recorder');
|
||||
it.only('Recorder View Request', () => {
|
||||
cy.get('.primary-action').should('contain', 'Start').click();
|
||||
|
||||
cy.visit('/app/List/DocType/List');
|
||||
|
|
@ -64,9 +66,5 @@ context('Recorder', () => {
|
|||
|
||||
cy.url().should('include', '/recorder/request');
|
||||
cy.get('form').should('contain', '/api/method/frappe');
|
||||
|
||||
cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
|
||||
cy.wait(200);
|
||||
cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,9 +18,14 @@ import os, sys, importlib, inspect, json
|
|||
from past.builtins import cmp
|
||||
import click
|
||||
|
||||
# public
|
||||
# Local application imports
|
||||
from .exceptions import *
|
||||
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
|
||||
from .utils.lazy_loader import lazy_import
|
||||
|
||||
# Lazy imports
|
||||
faker = lazy_import('faker')
|
||||
|
||||
|
||||
# Harmless for Python 3
|
||||
# For Python 2 set default encoding to utf-8
|
||||
|
|
@ -1749,15 +1754,13 @@ def parse_json(val):
|
|||
return parse_json(val)
|
||||
|
||||
def mock(type, size=1, locale='en'):
|
||||
from faker import Faker
|
||||
|
||||
results = []
|
||||
faker = Faker(locale)
|
||||
if not type in dir(faker):
|
||||
fake = faker.Faker(locale)
|
||||
if type not in dir(fake):
|
||||
raise ValueError('Not a valid mock type.')
|
||||
else:
|
||||
for i in range(size):
|
||||
data = getattr(faker, type)()
|
||||
data = getattr(fake, type)()
|
||||
results.append(data)
|
||||
|
||||
from frappe.chat.util import squashify
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import frappe
|
|||
from frappe.utils.minify import JavascriptMinify
|
||||
|
||||
import click
|
||||
import psutil
|
||||
from six import iteritems, text_type
|
||||
from six.moves.urllib.parse import urlparse
|
||||
|
||||
|
|
@ -226,7 +227,7 @@ def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False,
|
|||
|
||||
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], ".."))
|
||||
check_yarn()
|
||||
frappe.commands.popen(command, cwd=frappe_app_path)
|
||||
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env())
|
||||
|
||||
|
||||
def watch(no_compress):
|
||||
|
|
@ -238,13 +239,32 @@ def watch(no_compress):
|
|||
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], ".."))
|
||||
check_yarn()
|
||||
frappe_app_path = frappe.get_app_path("frappe", "..")
|
||||
frappe.commands.popen("{pacman} run watch".format(pacman=pacman), cwd=frappe_app_path)
|
||||
frappe.commands.popen("{pacman} run watch".format(pacman=pacman),
|
||||
cwd=frappe_app_path, env=get_node_env())
|
||||
|
||||
|
||||
def check_yarn():
|
||||
if not find_executable("yarn"):
|
||||
print("Please install yarn using below command and try again.\nnpm install -g yarn")
|
||||
|
||||
def get_node_env():
|
||||
node_env = {
|
||||
"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"
|
||||
}
|
||||
return node_env
|
||||
|
||||
def get_safe_max_old_space_size():
|
||||
safe_max_old_space_size = 0
|
||||
try:
|
||||
total_memory = psutil.virtual_memory().total / (1024 * 1024)
|
||||
# reference for the safe limit assumption
|
||||
# https://nodejs.org/api/cli.html#cli_max_old_space_size_size_in_megabytes
|
||||
# set minimum value 1GB
|
||||
safe_max_old_space_size = max(1024, int(total_memory * 0.75))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return safe_max_old_space_size
|
||||
|
||||
def make_asset_dirs(make_copy=False, restore=False):
|
||||
# don't even think of making assets_path absolute - rm -rf ahead.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import frappe.utils
|
|||
import subprocess # nosec
|
||||
from functools import wraps
|
||||
from six import StringIO
|
||||
from os import environ
|
||||
|
||||
click.disable_unicode_literals_warning = True
|
||||
|
||||
|
|
@ -53,16 +54,20 @@ def get_site(context, raise_err=True):
|
|||
return None
|
||||
|
||||
def popen(command, *args, **kwargs):
|
||||
output = kwargs.get('output', True)
|
||||
cwd = kwargs.get('cwd')
|
||||
shell = kwargs.get('shell', True)
|
||||
output = kwargs.get('output', True)
|
||||
cwd = kwargs.get('cwd')
|
||||
shell = kwargs.get('shell', True)
|
||||
raise_err = kwargs.get('raise_err')
|
||||
env = kwargs.get('env')
|
||||
if env:
|
||||
env = dict(environ, **env)
|
||||
|
||||
proc = subprocess.Popen(command,
|
||||
stdout = None if output else subprocess.PIPE,
|
||||
stderr = None if output else subprocess.PIPE,
|
||||
shell = shell,
|
||||
cwd = cwd
|
||||
stdout=None if output else subprocess.PIPE,
|
||||
stderr=None if output else subprocess.PIPE,
|
||||
shell=shell,
|
||||
cwd=cwd,
|
||||
env=env
|
||||
)
|
||||
|
||||
return_ = proc.wait()
|
||||
|
|
|
|||
|
|
@ -578,7 +578,7 @@ def run_ui_tests(context, app, headless=False):
|
|||
frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 --no-lockfile")
|
||||
|
||||
# run for headless mode
|
||||
run_or_open = 'run --browser chrome --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
|
||||
run_or_open = 'run --browser firefox --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
|
||||
command = '{site_env} {password_env} {cypress} {run_or_open}'
|
||||
formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open)
|
||||
|
||||
|
|
|
|||
|
|
@ -97,11 +97,16 @@ class Contact(Document):
|
|||
if len([email.email_id for email in self.email_ids if email.is_primary]) > 1:
|
||||
frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold("Email ID")))
|
||||
|
||||
primary_email_exists = False
|
||||
for d in self.email_ids:
|
||||
if d.is_primary == 1:
|
||||
primary_email_exists = True
|
||||
self.email_id = d.email_id.strip()
|
||||
break
|
||||
|
||||
if not primary_email_exists:
|
||||
self.email_id = ""
|
||||
|
||||
def set_primary(self, fieldname):
|
||||
# Used to set primary mobile and phone no.
|
||||
if len(self.phone_nos) == 0:
|
||||
|
|
@ -115,11 +120,16 @@ class Contact(Document):
|
|||
if len(is_primary) > 1:
|
||||
frappe.throw(_("Only one {0} can be set as primary.").format(frappe.bold(frappe.unscrub(fieldname))))
|
||||
|
||||
primary_number_exists = False
|
||||
for d in self.phone_nos:
|
||||
if d.get(field_name) == 1:
|
||||
primary_number_exists = True
|
||||
setattr(self, fieldname, d.phone)
|
||||
break
|
||||
|
||||
if not primary_number_exists:
|
||||
setattr(self, fieldname, "")
|
||||
|
||||
def get_default_contact(doctype, name):
|
||||
'''Returns default contact for the given doctype, name'''
|
||||
out = frappe.db.sql('''select parent,
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ class TestAddressesAndContacts(unittest.TestCase):
|
|||
create_linked_contact(links_list, d)
|
||||
report_data = get_data({"reference_doctype": "Test Custom Doctype"})
|
||||
for idx, link in enumerate(links_list):
|
||||
test_item = [link, 'test address line 1', 'test address line 2', 'Milan', None, None, 'Italy', 0, '_Test First Name', '_Test Last Name', '_Test Address-Billing', '+91 0000000000', None, 'test_contact@example.com', 1]
|
||||
test_item = [link, 'test address line 1', 'test address line 2', 'Milan', None, None, 'Italy', 0, '_Test First Name', '_Test Last Name', '_Test Address-Billing', '+91 0000000000', '', 'test_contact@example.com', 1]
|
||||
self.assertListEqual(test_item, report_data[idx])
|
||||
|
||||
def tearDown(self):
|
||||
|
|
|
|||
|
|
@ -449,8 +449,8 @@ class ImportFile:
|
|||
data_without_first_row = data[1:]
|
||||
for row in data_without_first_row:
|
||||
row_values = row.get_values(parent_column_indexes)
|
||||
# if the row is blank, it's a child row doc
|
||||
if all([v in INVALID_VALUES for v in row_values]):
|
||||
# if the row is blank or same content as the previous parent row, it's a child row doc
|
||||
if all([v in INVALID_VALUES for v in row_values]) or row_values == parent_row_values:
|
||||
rows.append(row)
|
||||
continue
|
||||
# if we encounter a row which has values in parent columns,
|
||||
|
|
@ -472,32 +472,6 @@ class ImportFile:
|
|||
|
||||
doc = parent_doc
|
||||
|
||||
if self.import_type == INSERT:
|
||||
# check if there is atleast one row for mandatory table fields
|
||||
meta = frappe.get_meta(self.doctype)
|
||||
mandatory_table_fields = [
|
||||
df
|
||||
for df in meta.fields
|
||||
if df.fieldtype in table_fieldtypes
|
||||
and df.reqd
|
||||
and len(doc.get(df.fieldname, [])) == 0
|
||||
]
|
||||
if len(mandatory_table_fields) == 1:
|
||||
self.warnings.append(
|
||||
{
|
||||
"row": first_row.row_number,
|
||||
"message": _("There should be atleast one row for {0} table").format(
|
||||
frappe.bold(mandatory_table_fields[0].label)
|
||||
),
|
||||
}
|
||||
)
|
||||
elif mandatory_table_fields:
|
||||
fields_string = ", ".join([df.label for df in mandatory_table_fields])
|
||||
message = _("There should be atleast one row for the following tables: {0}").format(
|
||||
fields_string
|
||||
)
|
||||
self.warnings.append({"row": first_row.row_number, "message": message})
|
||||
|
||||
return doc, rows, data[len(rows) :]
|
||||
|
||||
def get_warnings(self):
|
||||
|
|
@ -626,7 +600,6 @@ class Row:
|
|||
new_doc.update(doc)
|
||||
doc = new_doc
|
||||
|
||||
self.check_mandatory_fields(doctype, doc, table_df)
|
||||
return doc
|
||||
|
||||
def validate_value(self, value, col):
|
||||
|
|
@ -727,66 +700,6 @@ class Row:
|
|||
pass
|
||||
return value
|
||||
|
||||
def check_mandatory_fields(self, doctype, doc, table_df=None):
|
||||
"""If import type is Insert:
|
||||
Check for mandatory fields (except table fields) in doc
|
||||
if import type is Update:
|
||||
Check for name field or autoname field in doc
|
||||
"""
|
||||
meta = frappe.get_meta(doctype)
|
||||
if self.import_type == UPDATE:
|
||||
if meta.istable:
|
||||
# when updating records with table rows,
|
||||
# there are two scenarios:
|
||||
# 1. if row 'name' is provided in the template
|
||||
# the table row will be updated
|
||||
# 2. if row 'name' is not provided
|
||||
# then a new row will be added
|
||||
# so we dont need to check for mandatory
|
||||
return
|
||||
|
||||
# for update, only ID (name) field is mandatory
|
||||
id_field = get_id_field(doctype)
|
||||
if doc.get(id_field.fieldname) in INVALID_VALUES:
|
||||
self.warnings.append(
|
||||
{
|
||||
"row": self.row_number,
|
||||
"message": _("{0} is a mandatory field").format(id_field.label),
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
fields = [
|
||||
df
|
||||
for df in meta.fields
|
||||
if df.fieldtype not in table_fieldtypes
|
||||
and df.reqd
|
||||
and doc.get(df.fieldname) in INVALID_VALUES
|
||||
]
|
||||
|
||||
if not fields:
|
||||
return
|
||||
|
||||
def get_field_label(df):
|
||||
return "{0}{1}".format(df.label, " ({})".format(table_df.label) if table_df else "")
|
||||
|
||||
if len(fields) == 1:
|
||||
field_label = get_field_label(fields[0])
|
||||
self.warnings.append(
|
||||
{
|
||||
"row": self.row_number,
|
||||
"message": _("{0} is a mandatory field").format(frappe.bold(field_label)),
|
||||
}
|
||||
)
|
||||
else:
|
||||
fields_string = ", ".join([frappe.bold(get_field_label(df)) for df in fields])
|
||||
self.warnings.append(
|
||||
{
|
||||
"row": self.row_number,
|
||||
"message": _("{0} are mandatory fields").format(fields_string),
|
||||
}
|
||||
)
|
||||
|
||||
def get_values(self, indexes):
|
||||
return [self.data[i] for i in indexes]
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ doctype_name = 'DocType for Import'
|
|||
class TestImporter(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
create_doctype_if_not_exists(doctype_name)
|
||||
create_doctype_if_not_exists(doctype_name,)
|
||||
|
||||
def test_data_import_from_file(self):
|
||||
import_file = get_import_file('sample_import_file')
|
||||
|
|
@ -59,18 +59,18 @@ class TestImporter(unittest.TestCase):
|
|||
def test_data_import_without_mandatory_values(self):
|
||||
import_file = get_import_file('sample_import_file_without_mandatory')
|
||||
data_import = self.get_importer(doctype_name, import_file)
|
||||
frappe.local.message_log = []
|
||||
data_import.start_import()
|
||||
data_import.reload()
|
||||
warnings = frappe.parse_json(data_import.template_warnings)
|
||||
import_log = frappe.parse_json(data_import.import_log)
|
||||
self.assertEqual(import_log[0]['row_indexes'], [2,3])
|
||||
expected_error = "Error: <b>Child 1 of DocType for Import</b> Row #1: Value missing for: Child Title"
|
||||
self.assertEqual(frappe.parse_json(import_log[0]['messages'][0])['message'], expected_error)
|
||||
expected_error = "Error: <b>Child 1 of DocType for Import</b> Row #2: Value missing for: Child Title"
|
||||
self.assertEqual(frappe.parse_json(import_log[0]['messages'][1])['message'], expected_error)
|
||||
|
||||
self.assertEqual(warnings[0]['row'], 2)
|
||||
self.assertEqual(warnings[0]['message'], "<b>Child Title (Table Field 1)</b> is a mandatory field")
|
||||
|
||||
self.assertEqual(warnings[1]['row'], 3)
|
||||
self.assertEqual(warnings[1]['message'], "<b>Child Title (Table Field 1 Again)</b> is a mandatory field")
|
||||
|
||||
self.assertEqual(warnings[2]['row'], 4)
|
||||
self.assertEqual(warnings[2]['message'], "<b>Title</b> is a mandatory field")
|
||||
self.assertEqual(import_log[1]['row_indexes'], [4])
|
||||
self.assertEqual(frappe.parse_json(import_log[1]['messages'][0])['message'], "Title is required")
|
||||
|
||||
def test_data_import_update(self):
|
||||
existing_doc = frappe.get_doc(
|
||||
|
|
@ -104,6 +104,8 @@ class TestImporter(unittest.TestCase):
|
|||
data_import.reference_doctype = doctype
|
||||
data_import.import_file = import_file.file_url
|
||||
data_import.insert()
|
||||
# Commit so that the first import failure does not rollback the Data Import insert.
|
||||
frappe.db.commit()
|
||||
|
||||
return data_import
|
||||
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@
|
|||
"issingle": 1,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-11-01 12:57:20.943845",
|
||||
"modified": "2021-03-02 18:06:00.868688",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "SMS Settings",
|
||||
|
|
@ -233,6 +233,6 @@
|
|||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"track_changes": 0,
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from frappe.utils import get_url
|
|||
from frappe.core.doctype.user.user import get_total_users
|
||||
from frappe.core.doctype.user.user import MaxUsersReachedError, test_password_strength
|
||||
from frappe.core.doctype.user.user import extract_mentions
|
||||
from frappe.frappeclient import FrappeClient
|
||||
|
||||
test_records = frappe.get_test_records('User')
|
||||
|
||||
|
|
@ -229,16 +230,22 @@ class TestUser(unittest.TestCase):
|
|||
self.assertEqual(extract_mentions(comment)[1], "test.again@example1.com")
|
||||
|
||||
def test_rate_limiting_for_reset_password(self):
|
||||
from frappe.utils.password import delete_password_reset_cache
|
||||
delete_password_reset_cache()
|
||||
|
||||
# Allow only one reset request for a day
|
||||
frappe.db.set_value("System Settings", "System Settings", "password_reset_limit", 1)
|
||||
frappe.db.commit()
|
||||
|
||||
user = frappe.get_doc("User", "testperm@example.com")
|
||||
link = user.reset_password()
|
||||
self.assertRegex(link, "\/update-password\?key=[A-Za-z0-9]*")
|
||||
url = get_url()
|
||||
data={'cmd': 'frappe.core.doctype.user.user.reset_password', 'user': 'test@test.com'}
|
||||
|
||||
self.assertRaises(frappe.ValidationError, user.reset_password, False)
|
||||
# Clear rate limit tracker to start fresh
|
||||
key = f"rl:{data['cmd']}:{data['user']}"
|
||||
frappe.cache().delete(key)
|
||||
|
||||
c = FrappeClient(url)
|
||||
res1 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
|
||||
res2 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
|
||||
self.assertEqual(res1.status_code, 200)
|
||||
self.assertEqual(res2.status_code, 417)
|
||||
|
||||
def test_user_rollback(self):
|
||||
""" """
|
||||
|
|
|
|||
|
|
@ -2,21 +2,25 @@
|
|||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals, print_function
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
import frappe
|
||||
import frappe.share
|
||||
import frappe.defaults
|
||||
import frappe.permissions
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, flt, has_gravatar, escape_html, format_datetime, now_datetime, get_formatted_email, today
|
||||
from frappe import throw, msgprint, _
|
||||
from frappe.utils.password import update_password as _update_password, check_password
|
||||
from frappe.utils.password import update_password as _update_password, check_password, get_password_reset_limit
|
||||
from frappe.desk.notifications import clear_notifications
|
||||
from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings
|
||||
from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings, toggle_notifications
|
||||
from frappe.utils.user import get_system_managers
|
||||
from bs4 import BeautifulSoup
|
||||
import frappe.permissions
|
||||
import frappe.share
|
||||
import frappe.defaults
|
||||
from frappe.website.utils import is_signup_enabled
|
||||
from frappe.rate_limiter import rate_limit
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
|
||||
|
||||
STANDARD_USERS = ("Guest", "Administrator")
|
||||
|
||||
|
||||
|
|
@ -146,6 +150,9 @@ class User(Document):
|
|||
if not cint(self.enabled) and getattr(frappe.local, "login_manager", None):
|
||||
frappe.local.login_manager.logout(user=self.name)
|
||||
|
||||
# toggle notifications based on the user's status
|
||||
toggle_notifications(self.name, enable=cint(self.enabled))
|
||||
|
||||
def add_system_manager_role(self):
|
||||
# if adding system manager, do nothing
|
||||
if not cint(self.enabled) or ("System Manager" in [user_role.role for user_role in
|
||||
|
|
@ -238,11 +245,6 @@ class User(Document):
|
|||
def reset_password(self, send_email=False, password_expired=False):
|
||||
from frappe.utils import random_string, get_url
|
||||
|
||||
rate_limit = frappe.db.get_single_value("System Settings", "password_reset_limit")
|
||||
|
||||
if rate_limit:
|
||||
check_password_reset_limit(self.name, rate_limit)
|
||||
|
||||
key = random_string(32)
|
||||
self.db_set("reset_password_key", key)
|
||||
|
||||
|
|
@ -254,7 +256,6 @@ class User(Document):
|
|||
if send_email:
|
||||
self.password_reset_mail(link)
|
||||
|
||||
update_password_reset_limit(self.name)
|
||||
return link
|
||||
|
||||
def get_other_system_managers(self):
|
||||
|
|
@ -358,6 +359,9 @@ class User(Document):
|
|||
set `user`=null
|
||||
where `user`=%s""", (self.name))
|
||||
|
||||
# delete notification settings
|
||||
frappe.delete_doc("Notification Settings", self.name, ignore_permissions=True)
|
||||
|
||||
|
||||
def before_rename(self, old_name, new_name, merge=False):
|
||||
self.check_demo()
|
||||
|
|
@ -837,6 +841,7 @@ def sign_up(email, full_name, redirect_to):
|
|||
return 2, _("Please ask your administrator to verify your sign-up")
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(key='user', limit=get_password_reset_limit, seconds = 24*60*60, methods=['POST'])
|
||||
def reset_password(user):
|
||||
if user=="Administrator":
|
||||
return 'not allowed'
|
||||
|
|
@ -1168,16 +1173,3 @@ def generate_keys(user):
|
|||
def switch_theme(theme):
|
||||
if theme in ["Dark", "Light"]:
|
||||
frappe.db.set_value("User", frappe.session.user, "desk_theme", theme)
|
||||
|
||||
def update_password_reset_limit(user):
|
||||
generated_link_count = get_generated_link_count(user)
|
||||
generated_link_count += 1
|
||||
frappe.cache().hset("password_reset_link_count", user, generated_link_count)
|
||||
|
||||
def check_password_reset_limit(user, rate_limit):
|
||||
generated_link_count = get_generated_link_count(user)
|
||||
if generated_link_count >= rate_limit:
|
||||
frappe.throw(_("You have reached the hourly limit for generating password reset links. Please try again later."))
|
||||
|
||||
def get_generated_link_count(user):
|
||||
return cint(frappe.cache().hget("password_reset_link_count", user)) or 0
|
||||
|
|
|
|||
|
|
@ -28,6 +28,16 @@ class BackgroundJobs {
|
|||
}
|
||||
});
|
||||
|
||||
// add a "Remove Failed Jobs button"
|
||||
this.remove_failed_button = this.page.add_inner_button(__("Remove Failed Jobs"), () => {
|
||||
frappe.call({
|
||||
method: 'frappe.core.page.background_jobs.background_jobs.remove_failed_jobs',
|
||||
callback: () => {
|
||||
this.refresh_jobs();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(frappe.render_template('background_jobs_outer')).appendTo(this.page.body);
|
||||
this.content = $(this.page.body).find('.table-area');
|
||||
}
|
||||
|
|
@ -62,4 +72,4 @@ class BackgroundJobs {
|
|||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,58 +1,88 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
|
||||
from rq import Queue, Worker
|
||||
from frappe.utils.background_jobs import get_redis_conn
|
||||
from frappe.utils import format_datetime, cint, convert_utc_to_user_timezone
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
from frappe import _
|
||||
|
||||
colors = {
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import convert_utc_to_user_timezone, format_datetime
|
||||
from frappe.utils.background_jobs import get_redis_conn
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rq.job import Job
|
||||
|
||||
JOB_COLORS = {
|
||||
'queued': 'orange',
|
||||
'failed': 'red',
|
||||
'started': 'blue',
|
||||
'finished': 'green'
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_info(show_failed=False):
|
||||
def get_info(show_failed=False) -> List[Dict]:
|
||||
if isinstance(show_failed, str):
|
||||
show_failed = json.loads(show_failed)
|
||||
|
||||
conn = get_redis_conn()
|
||||
queues = Queue.all(conn)
|
||||
workers = Worker.all(conn)
|
||||
jobs = []
|
||||
|
||||
def add_job(j, name):
|
||||
if j.kwargs.get('site')==frappe.local.site:
|
||||
jobs.append({
|
||||
'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') \
|
||||
or j.kwargs.get('kwargs', {}).get('job_type') \
|
||||
or str(j.kwargs.get('job_name')),
|
||||
'status': j.get_status(), 'queue': name,
|
||||
'creation': format_datetime(convert_utc_to_user_timezone(j.created_at)),
|
||||
'color': colors[j.get_status()]
|
||||
})
|
||||
if j.exc_info:
|
||||
jobs[-1]['exc_info'] = j.exc_info
|
||||
def add_job(job: 'Job', name: str) -> None:
|
||||
if job.kwargs.get('site') == frappe.local.site:
|
||||
job_info = {
|
||||
'job_name': job.kwargs.get('kwargs', {}).get('playbook_method')
|
||||
or job.kwargs.get('kwargs', {}).get('job_type')
|
||||
or str(job.kwargs.get('job_name')),
|
||||
'status': job.get_status(),
|
||||
'queue': name,
|
||||
'creation': format_datetime(convert_utc_to_user_timezone(job.created_at)),
|
||||
'color': JOB_COLORS[job.get_status()]
|
||||
}
|
||||
|
||||
for w in workers:
|
||||
j = w.get_current_job()
|
||||
if j:
|
||||
add_job(j, w.name)
|
||||
if job.exc_info:
|
||||
job_info['exc_info'] = job.exc_info
|
||||
|
||||
for q in queues:
|
||||
if q.name != 'failed':
|
||||
for j in q.get_jobs(): add_job(j, q.name)
|
||||
jobs.append(job_info)
|
||||
|
||||
if cint(show_failed):
|
||||
for q in queues:
|
||||
if q.name == 'failed':
|
||||
for j in q.get_jobs()[:10]: add_job(j, q.name)
|
||||
# show worker jobs
|
||||
for worker in workers:
|
||||
job = worker.get_current_job()
|
||||
if job:
|
||||
add_job(job, worker.name)
|
||||
|
||||
for queue in queues:
|
||||
# show active queued jobs
|
||||
if queue.name != 'failed':
|
||||
for job in queue.jobs:
|
||||
add_job(job, queue.name)
|
||||
|
||||
# show failed jobs, if requested
|
||||
if show_failed:
|
||||
fail_registry = queue.failed_job_registry
|
||||
for job_id in fail_registry.get_job_ids():
|
||||
job = queue.fetch_job(job_id)
|
||||
add_job(job, queue.name)
|
||||
|
||||
return jobs
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove_failed_jobs():
|
||||
conn = get_redis_conn()
|
||||
queues = Queue.all(conn)
|
||||
for queue in queues:
|
||||
fail_registry = queue.failed_job_registry
|
||||
for job_id in fail_registry.get_job_ids():
|
||||
job = queue.fetch_job(job_id)
|
||||
fail_registry.remove(job, delete_job=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_scheduler_status():
|
||||
if is_scheduler_inactive():
|
||||
|
|
|
|||
|
|
@ -36,17 +36,17 @@ class Dashboard {
|
|||
} else {
|
||||
// last opened
|
||||
if (frappe.last_dashboard) {
|
||||
frappe.set_route('dashboard-view', frappe.last_dashboard);
|
||||
frappe.set_re_route('dashboard-view', frappe.last_dashboard);
|
||||
} else {
|
||||
// default dashboard
|
||||
frappe.db.get_list('Dashboard', {filters: {is_default: 1}}).then(data => {
|
||||
if (data && data.length) {
|
||||
frappe.set_route('dashboard-view', data[0].name);
|
||||
frappe.set_re_route('dashboard-view', data[0].name);
|
||||
} else {
|
||||
// no default, get the latest one
|
||||
frappe.db.get_list('Dashboard', {limit: 1}).then(data => {
|
||||
if (data && data.length) {
|
||||
frappe.set_route('dashboard-view', data[0].name);
|
||||
frappe.set_re_route('dashboard-view', data[0].name);
|
||||
} else {
|
||||
// create a new dashboard!
|
||||
frappe.new_doc('Dashboard');
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class Recorder {
|
|||
}
|
||||
|
||||
show() {
|
||||
|
||||
if (!this.view || this.view.$route.name == "recorder-detail") return;
|
||||
this.view.$router.replace({name: "recorder-detail"});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ frappe.ui.form.on("Customize Form", {
|
|||
frm.trigger("setup_sortable");
|
||||
}
|
||||
}
|
||||
localStorage["customize_doctype"] = frm.doc.doc_type;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ def create_notification_settings(user):
|
|||
_doc.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def toggle_notifications(user, enable=False):
|
||||
if frappe.db.exists("Notification Settings", user):
|
||||
frappe.db.set_value("Notification Settings", user, 'enabled', enable)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_subscribed_documents():
|
||||
if not frappe.session.user:
|
||||
|
|
@ -75,4 +80,4 @@ def get_permission_query_conditions(user):
|
|||
|
||||
@frappe.whitelist()
|
||||
def set_seen_value(value, user):
|
||||
frappe.db.set_value('Notification Settings', user, 'seen', value, update_modified=False)
|
||||
frappe.db.set_value('Notification Settings', user, 'seen', value, update_modified=False)
|
||||
|
|
|
|||
|
|
@ -16,8 +16,18 @@ def get_leaderboards():
|
|||
|
||||
@frappe.whitelist()
|
||||
def get_energy_point_leaderboard(date_range, company = None, field = None, limit = None):
|
||||
all_users = frappe.db.get_all('User',
|
||||
filters = {
|
||||
'name': ['not in', ['Administrator', 'Guest']],
|
||||
'enabled': 1,
|
||||
'user_type': ['!=', 'Website User']
|
||||
},
|
||||
order_by = 'name ASC')
|
||||
all_users_list = list(map(lambda x: x['name'], all_users))
|
||||
|
||||
filters = [
|
||||
['type', '!=', 'Review'],
|
||||
['user', 'in', all_users_list]
|
||||
]
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
|
|
@ -28,15 +38,7 @@ def get_energy_point_leaderboard(date_range, company = None, field = None, limit
|
|||
group_by = 'user',
|
||||
order_by = 'value desc'
|
||||
)
|
||||
all_users = frappe.db.get_all('User',
|
||||
filters = {
|
||||
'name': ['not in', ['Administrator', 'Guest']],
|
||||
'enabled': 1,
|
||||
'user_type': ['!=', 'Website User']
|
||||
},
|
||||
order_by = 'name ASC')
|
||||
|
||||
all_users_list = list(map(lambda x: x['name'], all_users))
|
||||
energy_point_users_list = list(map(lambda x: x['name'], energy_point_users))
|
||||
for user in all_users_list:
|
||||
if user not in energy_point_users_list:
|
||||
|
|
|
|||
|
|
@ -207,8 +207,7 @@ scheduler_events = {
|
|||
"frappe.deferred_insert.save_to_db",
|
||||
"frappe.desk.form.document_follow.send_hourly_updates",
|
||||
"frappe.integrations.doctype.google_calendar.google_calendar.sync",
|
||||
"frappe.email.doctype.newsletter.newsletter.send_scheduled_email",
|
||||
"frappe.utils.password.delete_password_reset_cache"
|
||||
"frappe.email.doctype.newsletter.newsletter.send_scheduled_email"
|
||||
],
|
||||
"daily": [
|
||||
"frappe.email.queue.set_expiry_for_email_queue",
|
||||
|
|
|
|||
|
|
@ -131,12 +131,10 @@ def upload_from_folder(path, is_private, dropbox_folder, dropbox_client, did_not
|
|||
|
||||
for f in frappe.get_all("File", filters={"is_folder": 0, "is_private": is_private,
|
||||
"uploaded_to_dropbox": 0}, fields=['file_url', 'name', 'file_name']):
|
||||
if is_private:
|
||||
filename = f.file_url.replace('/private/files/', '')
|
||||
else:
|
||||
if not f.file_url:
|
||||
f.file_url = '/files/' + f.file_name;
|
||||
filename = f.file_url.replace('/files/', '')
|
||||
if not f.file_url:
|
||||
continue
|
||||
filename = f.file_url.rsplit('/', 1)[-1]
|
||||
|
||||
filepath = os.path.join(path, filename)
|
||||
|
||||
if filename in ignore_list:
|
||||
|
|
|
|||
|
|
@ -331,3 +331,5 @@ execute:frappe.get_doc('Role', 'Guest').save() # remove desk access
|
|||
frappe.patches.v13_0.rename_desk_page_to_workspace # 02.02.2021
|
||||
frappe.patches.v13_0.delete_package_publish_tool
|
||||
frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings
|
||||
frappe.patches.v13_0.remove_twilio_settings
|
||||
frappe.patches.v12_0.rename_uploaded_files_with_proper_name
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import frappe
|
||||
import os
|
||||
|
||||
def execute():
|
||||
file_names_with_url = frappe.get_all("File", filters={
|
||||
"is_folder": 0,
|
||||
"file_name": ["like", "%/%"]
|
||||
}, fields=['name', 'file_name', 'file_url'])
|
||||
|
||||
for f in file_names_with_url:
|
||||
filename = f.file_name.rsplit('/', 1)[-1]
|
||||
|
||||
if not f.file_url:
|
||||
f.file_url = f.file_name
|
||||
|
||||
try:
|
||||
if not file_exists(f.file_url):
|
||||
continue
|
||||
frappe.db.set_value('File', f.name, {
|
||||
"file_name": filename,
|
||||
"file_url": f.file_url
|
||||
}, update_modified=False)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def file_exists(file_path):
|
||||
file_path = frappe.utils.get_files_path(
|
||||
file_path.rsplit('/', 1)[-1],
|
||||
is_private=file_path.startswith('/private')
|
||||
)
|
||||
return os.path.exists(file_path)
|
||||
20
frappe/patches/v13_0/remove_twilio_settings.py
Normal file
20
frappe/patches/v13_0/remove_twilio_settings.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
"""Add missing Twilio patch.
|
||||
|
||||
While making Twilio as a standaone app, we missed to delete Twilio records from DB through migration. Adding the missing patch.
|
||||
"""
|
||||
frappe.delete_doc_if_exists('DocType', 'Twilio Number Group')
|
||||
if twilio_settings_doctype_in_integrations():
|
||||
frappe.delete_doc_if_exists('DocType', 'Twilio Settings')
|
||||
frappe.db.sql("delete from `tabSingles` where `doctype`=%s", 'Twilio Settings')
|
||||
|
||||
def twilio_settings_doctype_in_integrations() -> bool:
|
||||
"""Check Twilio Settings doctype exists in integrations module or not.
|
||||
"""
|
||||
return frappe.db.exists("DocType", {'name': 'Twilio Settings', 'module': 'Integrations'})
|
||||
|
|
@ -171,7 +171,7 @@
|
|||
"fieldname": "custom_html_help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Custom HTML Help",
|
||||
"options": "<h3>Custom CSS Help</h3>\n\n<p>Notes:</p>\n\n<ol>\n<li>All field groups (label + value) are set attributes <code>data-fieldtype</code> and <code>data-fieldname</code></li>\n<li>All values are given class <code>value</code></li>\n<li>All Section Breaks are given class <code>section-break</code></li>\n<li>All Column Breaks are given class <code>column-break</code></li>\n</ol>\n\n<h4>Examples</h4>\n\n<p>1. Left align integers</p>\n\n<pre><code>[data-fieldtype=\"Int\"] .value { text-left: left; }</code></pre>\n\n<p>1. Add border to sections except the last section</p>\n\n<pre><code>.section-break { padding: 30px 0px; border-bottom: 1px solid #eee; }\n.section-break:last-child { padding-bottom: 0px; border-bottom: 0px; }</code></pre>\n"
|
||||
"options": "<h3>Custom CSS Help</h3>\n\n<p>Notes:</p>\n\n<ol>\n<li>All field groups (label + value) are set attributes <code>data-fieldtype</code> and <code>data-fieldname</code></li>\n<li>All values are given class <code>value</code></li>\n<li>All Section Breaks are given class <code>section-break</code></li>\n<li>All Column Breaks are given class <code>column-break</code></li>\n</ol>\n\n<h4>Examples</h4>\n\n<p>1. Left align integers</p>\n\n<pre><code>[data-fieldtype=\"Int\"] .value { text-align: left; }</code></pre>\n\n<p>1. Add border to sections except the last section</p>\n\n<pre><code>.section-break { padding: 30px 0px; border-bottom: 1px solid #eee; }\n.section-break:last-child { padding-bottom: 0px; border-bottom: 0px; }</code></pre>\n"
|
||||
},
|
||||
{
|
||||
"depends_on": "custom_format",
|
||||
|
|
@ -211,7 +211,7 @@
|
|||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-14 11:38:49.132061",
|
||||
"modified": "2021-03-01 15:25:46.578863",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Printing",
|
||||
"name": "Print Format",
|
||||
|
|
|
|||
|
|
@ -269,6 +269,7 @@ frappe.ui.form.PrintView = class {
|
|||
based_on: data.based_on,
|
||||
};
|
||||
frappe.set_route('print-format-builder');
|
||||
this.print_sel.val(data.print_format_name);
|
||||
},
|
||||
__('New Custom Print Format'),
|
||||
__('Start')
|
||||
|
|
@ -641,10 +642,13 @@ frappe.ui.form.PrintView = class {
|
|||
|
||||
refresh_print_options() {
|
||||
this.print_formats = frappe.meta.get_print_formats(this.frm.doctype);
|
||||
return this.print_sel.empty().add_options([
|
||||
const print_format_select_val = this.print_sel.val();
|
||||
this.print_sel.empty().add_options([
|
||||
this.get_default_option_for_select(__('Select Print Format')),
|
||||
...this.print_formats
|
||||
]);
|
||||
return this.print_formats.includes(print_format_select_val)
|
||||
&& this.print_sel.val(print_format_select_val);
|
||||
}
|
||||
|
||||
selected_format() {
|
||||
|
|
|
|||
|
|
@ -784,6 +784,7 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
btn: this.page.btn_primary,
|
||||
callback: function(r) {
|
||||
me.print_format = r.message;
|
||||
locals['Print Format'][me.print_format.name] = r.message;
|
||||
frappe.show_alert({message: __("Saved"), indicator: 'green'});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
<span class="drag-handle">
|
||||
<svg class="icon icon-xs"><use xlink:href="#icon-drag"></use></svg>
|
||||
</span>
|
||||
{%= __(f.label) %}
|
||||
{%= __(f.label) || __(f.fieldname) %}
|
||||
</div>
|
||||
</div>
|
||||
{% } %}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
<svg width="6" height="8" viewBox="0 0 6 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.25 7.5L4.75 4L1.25 0.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 206 B |
|
|
@ -201,12 +201,13 @@ frappe.Application = Class.extend({
|
|||
|
||||
email_password_prompt: function(email_account,user,i) {
|
||||
var me = this;
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __('Email Account setup please enter your password for: {0}', [email_account[i]["email_id"]]),
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __('Password missing in Email Account'),
|
||||
fields: [
|
||||
{ 'fieldname': 'password',
|
||||
{
|
||||
'fieldname': 'password',
|
||||
'fieldtype': 'Password',
|
||||
'label': 'Email Account Password',
|
||||
'label': __('Please enter the password for: <b>{0}</b>', [email_account[i]["email_id"]]),
|
||||
'reqd': 1
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
|
||||
make_input: function() {
|
||||
var me = this;
|
||||
let me = this;
|
||||
this.$input = $('<button class="btn btn-default btn-sm btn-attach">')
|
||||
.html(__("Attach"))
|
||||
.prependTo(me.input_area)
|
||||
|
|
@ -28,7 +28,7 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
|
|||
this.toggle_reload_button();
|
||||
},
|
||||
clear_attachment: function() {
|
||||
var me = this;
|
||||
let me = this;
|
||||
if(this.frm) {
|
||||
me.parse_validate_and_set_in_model(null);
|
||||
me.refresh();
|
||||
|
|
@ -79,10 +79,13 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
|
|||
this.value = value;
|
||||
if(this.value) {
|
||||
this.$input.toggle(false);
|
||||
if(this.value.indexOf(",")!==-1) {
|
||||
var parts = this.value.split(",");
|
||||
var filename = parts[0];
|
||||
dataurl = parts[1];
|
||||
// value can also be using this format: FILENAME,DATA_URL
|
||||
// Important: We have to be careful because normal filenames may also contain ","
|
||||
let file_url_parts = this.value.match(/^([^:]+),(.+):(.+)$/);
|
||||
let filename;
|
||||
if (file_url_parts) {
|
||||
filename = file_url_parts[1];
|
||||
dataurl = file_url_parts[2] + ':' + file_url_parts[3];
|
||||
}
|
||||
this.$value.toggle(true).find(".attached-file-link")
|
||||
.html(filename || this.value)
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({
|
|||
label = is_label_null ? __(value) : __(v.label);
|
||||
}
|
||||
}
|
||||
|
||||
$('<option>').html(cstr(label))
|
||||
.attr('value', value)
|
||||
.prop('disabled', is_disabled)
|
||||
|
|
@ -129,6 +130,7 @@ frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({
|
|||
}
|
||||
// select the first option
|
||||
this.selectedIndex = 0;
|
||||
$(this).trigger('select-change');
|
||||
return $(this);
|
||||
};
|
||||
$.fn.set_working = function() {
|
||||
|
|
|
|||
|
|
@ -381,7 +381,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
method: method,
|
||||
args: {
|
||||
doctype: this.frm.doctype,
|
||||
name: this.frm.doc.name,
|
||||
name: this.frm.docname,
|
||||
items: items
|
||||
},
|
||||
callback: function(r) {
|
||||
|
|
@ -681,7 +681,7 @@ class Section {
|
|||
this.set_icon(hide);
|
||||
|
||||
// save state for next reload ('' is falsy)
|
||||
localStorage.setItem(this.df.css_class + '-closed', hide ? '1' : '');
|
||||
localStorage.setItem(this.df.css_class + '-closed', hide ? '1' : '');
|
||||
}
|
||||
|
||||
set_icon(hide) {
|
||||
|
|
@ -700,4 +700,4 @@ class Section {
|
|||
show() {
|
||||
this.wrapper.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ frappe.form.formatters = {
|
|||
return repl('<a onclick="%(onclick)s">%(value)s</a>',
|
||||
{onclick: docfield.link_onclick.replace(/"/g, '"'), value:value});
|
||||
} else if(docfield && doctype) {
|
||||
if (!frappe.model.can_select(doctype) && frappe.model.can_read(doctype)) {
|
||||
if (frappe.model.can_read(doctype)) {
|
||||
return `<a
|
||||
href="/app/${encodeURIComponent(frappe.router.slug(doctype))}/${encodeURIComponent(original_value)}"
|
||||
data-doctype="${doctype}"
|
||||
|
|
|
|||
|
|
@ -649,7 +649,7 @@ export default class Grid {
|
|||
duplicate_row(d, copy_doc) {
|
||||
$.each(copy_doc, function (key, value) {
|
||||
if (!["creation", "modified", "modified_by", "idx", "owner",
|
||||
"parent", "doctype", "name", "parentield"].includes(key)) {
|
||||
"parent", "doctype", "name", "parentfield"].includes(key)) {
|
||||
d[key] = value;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ frappe.ui.form.Layout = Class.extend({
|
|||
},
|
||||
|
||||
refresh_section_collapse: function () {
|
||||
if (!this.doc) return;
|
||||
if (!(this.sections && this.sections.length)) return;
|
||||
|
||||
for (var i = 0; i < this.sections.length; i++) {
|
||||
var section = this.sections[i];
|
||||
|
|
|
|||
|
|
@ -39,8 +39,7 @@ frappe.ui.form.AssignTo = Class.extend({
|
|||
avatar_group.click(() => {
|
||||
new frappe.ui.form.AssignmentDialog({
|
||||
assignments: assigned_users,
|
||||
frm: this.frm,
|
||||
remove_action: this.remove.bind(this)
|
||||
frm: this.frm
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
@ -84,7 +83,7 @@ frappe.ui.form.AssignTo = Class.extend({
|
|||
|
||||
|
||||
frappe.ui.form.AssignToDialog = Class.extend({
|
||||
init: function(opts){
|
||||
init: function(opts) {
|
||||
$.extend(this, opts);
|
||||
|
||||
this.make();
|
||||
|
|
@ -214,15 +213,35 @@ frappe.ui.form.AssignmentDialog = class {
|
|||
constructor(opts) {
|
||||
this.frm = opts.frm;
|
||||
this.assignments = opts.assignments;
|
||||
this.remove_action = opts.remove_action;
|
||||
this.make();
|
||||
}
|
||||
|
||||
make() {
|
||||
this.dialog = new frappe.ui.Dialog({
|
||||
title: __('Assigned To'),
|
||||
title: __('Assignments'),
|
||||
size: 'small',
|
||||
no_focus: true,
|
||||
fields: [{
|
||||
'label': __('Assign a user'),
|
||||
'fieldname': 'user',
|
||||
'fieldtype': 'Link',
|
||||
'options': 'User',
|
||||
'change': () => {
|
||||
let value = this.dialog.get_value('user');
|
||||
if (value && !this.assigning) {
|
||||
this.assigning = true;
|
||||
this.dialog.set_df_property('user', 'read_only', 1);
|
||||
this.dialog.set_df_property('user', 'description', __('Assigning...'));
|
||||
this.add_assignment(value).then(() => {
|
||||
this.dialog.set_value('user', null);
|
||||
}).finally(() => {
|
||||
this.dialog.set_df_property('user', 'description', null);
|
||||
this.dialog.set_df_property('user', 'read_only', 0);
|
||||
this.assigning = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}, {
|
||||
'fieldtype': 'HTML',
|
||||
'fieldname': 'assignment_list'
|
||||
}]
|
||||
|
|
@ -236,8 +255,31 @@ frappe.ui.form.AssignmentDialog = class {
|
|||
});
|
||||
this.dialog.show();
|
||||
}
|
||||
render(assignments) {
|
||||
this.frm && this.frm.assign_to.render(assignments);
|
||||
}
|
||||
add_assignment(assignment) {
|
||||
return frappe.xcall('frappe.desk.form.assign_to.add', {
|
||||
doctype: this.frm.doctype,
|
||||
name: this.frm.docname,
|
||||
assign_to: [assignment],
|
||||
}).then((assignments) => {
|
||||
this.update_assignment(assignment);
|
||||
this.render(assignments);
|
||||
});
|
||||
}
|
||||
remove_assignment(assignment) {
|
||||
return frappe.xcall('frappe.desk.form.assign_to.remove', {
|
||||
doctype: this.frm.doctype,
|
||||
name: this.frm.docname,
|
||||
assign_to: assignment,
|
||||
});
|
||||
}
|
||||
update_assignment(assignment) {
|
||||
this.assignment_list.append(this.get_assignment_row(assignment));
|
||||
const in_the_list = this.assignment_list.find(`[data-user="${assignment}"]`).length;
|
||||
if (!in_the_list) {
|
||||
this.assignment_list.append(this.get_assignment_row(assignment));
|
||||
}
|
||||
}
|
||||
get_assignment_row(assignment) {
|
||||
let row = $(`
|
||||
|
|
@ -256,10 +298,12 @@ frappe.ui.form.AssignmentDialog = class {
|
|||
</span>
|
||||
`);
|
||||
row.find('.remove-btn').click(() => {
|
||||
this.remove_action && this.remove_action(assignment);
|
||||
row.remove();
|
||||
this.remove_assignment(assignment).then((assignments) => {
|
||||
row.remove();
|
||||
this.render(assignments);
|
||||
});
|
||||
});
|
||||
}
|
||||
return row;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -156,10 +156,11 @@ frappe.views.ListViewSelect = class ListViewSelect {
|
|||
items.map(item => {
|
||||
if (item.name.toLowerCase() == page_name.toLowerCase()) {
|
||||
placeholder = item.name;
|
||||
} else {
|
||||
html += `<li><a class="dropdown-item" href="${item.route}">${
|
||||
item.name
|
||||
}</a></li>`;
|
||||
}
|
||||
html += `<li><a class="dropdown-item" href="${item.route}">${
|
||||
item.name
|
||||
}</a></li>`;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@
|
|||
<script>
|
||||
export default {
|
||||
name: "RecorderRoot",
|
||||
created() {
|
||||
this.$router.push({name: 'recorder-detail'});
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
frappe.router.current_route = frappe.router.parse();
|
||||
frappe.breadcrumbs.update();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -284,7 +284,7 @@ export default {
|
|||
frappe.breadcrumbs.add({
|
||||
type: 'Custom',
|
||||
label: __('Recorder'),
|
||||
route: '#recorder'
|
||||
route: '/app/recorder'
|
||||
});
|
||||
frappe.call({
|
||||
method: "frappe.recorder.get",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ const routes = [
|
|||
path: '/request/:id',
|
||||
component: RequestDetail,
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
redirect: {
|
||||
name: "recorder-detail"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const router = new VueRouter({
|
||||
|
|
@ -26,11 +32,11 @@ const router = new VueRouter({
|
|||
routes: routes,
|
||||
});
|
||||
|
||||
new Vue({
|
||||
frappe.recorder.view = new Vue({
|
||||
el: ".recorder-container",
|
||||
router: router,
|
||||
data: {
|
||||
page: cur_page.page.page
|
||||
page: frappe.pages["recorder"].page
|
||||
},
|
||||
template: "<recorder-root/>",
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ frappe.router = {
|
|||
let sub_path = this.get_sub_path();
|
||||
if (this.re_route(sub_path)) return;
|
||||
|
||||
this.current_sub_path = sub_path;
|
||||
this.current_route = this.parse();
|
||||
this.set_history(sub_path);
|
||||
this.render();
|
||||
|
|
@ -223,14 +224,14 @@ frappe.router = {
|
|||
// it doesn't allow us to go back to the one prior to "new-doctype-1"
|
||||
// Hence if this check is true, instead of changing location hash,
|
||||
// we just do a back to go to the doc previous to the "new-doctype-1"
|
||||
var re_route_val = this.get_sub_path(frappe.re_route[sub_path]);
|
||||
if (decodeURIComponent(re_route_val) !== decodeURIComponent(sub_path)) {
|
||||
const re_route_val = this.get_sub_path(frappe.re_route[sub_path]);
|
||||
if (re_route_val === this.current_sub_path) {
|
||||
window.history.back();
|
||||
return true;
|
||||
} else {
|
||||
frappe.set_route(re_route_val);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
|
|||
// make fields (if any)
|
||||
super.make();
|
||||
|
||||
this.refresh_section_collapse();
|
||||
|
||||
// show footer
|
||||
this.action = this.action || { primary: { }, secondary: { } };
|
||||
if (this.primary_action || (this.action.primary && this.action.primary.onsubmit)) {
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ frappe.msgprint = function(msg, title, is_minimizable) {
|
|||
}
|
||||
|
||||
frappe.msg_dialog.set_primary_action(
|
||||
__(data.primary_action.label || "Done"),
|
||||
__(data.primary_action.label || data.primary_action_label || "Done"),
|
||||
data.primary_action.action
|
||||
);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
|
|||
}
|
||||
|
||||
refresh() {
|
||||
this.current_theme = document.body.dataset.theme;
|
||||
this.current_theme = document.documentElement.getAttribute("data-theme") || "light";
|
||||
this.fetch_themes().then(() => {
|
||||
this.render();
|
||||
});
|
||||
|
|
@ -45,7 +45,6 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
get_preview_html(theme) {
|
||||
const preview = $(`<div class="${this.current_theme == theme.name ? "selected" : "" }">
|
||||
<div data-theme=${theme.name}>
|
||||
|
|
@ -69,15 +68,9 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
|
|||
</div>
|
||||
</div>`);
|
||||
|
||||
// preview.on('mouseover', () => {
|
||||
// this.toggle_theme(theme.name, true)
|
||||
// })
|
||||
|
||||
// preview.on('mouseleave', () => {
|
||||
// this.toggle_theme(this.current_theme, true)
|
||||
// })
|
||||
|
||||
preview.on('click', () => {
|
||||
if (this.current_theme === theme.name) return;
|
||||
|
||||
this.themes.forEach((th) => {
|
||||
th.$html.removeClass("selected");
|
||||
});
|
||||
|
|
@ -89,19 +82,15 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
|
|||
return preview;
|
||||
}
|
||||
|
||||
toggle_theme(theme, preview=false) {
|
||||
if (!preview) {
|
||||
document.body.dataset.theme = theme.toLowerCase();
|
||||
frappe.show_alert("Theme Changed", 3);
|
||||
toggle_theme(theme) {
|
||||
this.current_theme = theme.toLowerCase();
|
||||
document.documentElement.setAttribute("data-theme", this.current_theme);
|
||||
frappe.show_alert("Theme Changed", 3);
|
||||
|
||||
frappe.call('frappe.core.doctype.user.user.switch_theme', {
|
||||
theme: toTitle(theme)
|
||||
});
|
||||
} else {
|
||||
document.body.dataset.theme = theme.toLowerCase();
|
||||
}
|
||||
frappe.xcall("frappe.core.doctype.user.user.switch_theme", {
|
||||
theme: toTitle(theme)
|
||||
});
|
||||
}
|
||||
|
||||
show() {
|
||||
this.dialog.show();
|
||||
}
|
||||
|
|
@ -109,4 +98,4 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
|
|||
hide() {
|
||||
this.dialog.hide();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -203,7 +203,9 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
if(this.dialog.fields_dict.sender) {
|
||||
this.dialog.fields_dict.sender.set_value(this.sender || '');
|
||||
}
|
||||
this.dialog.fields_dict.subject.set_value(this.subject || '');
|
||||
this.dialog.fields_dict.subject.set_value(
|
||||
frappe.utils.html2text(this.subject) || ''
|
||||
);
|
||||
|
||||
this.setup_earlier_reply();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ frappe.views.FormFactory = class FormFactory extends frappe.views.Factory {
|
|||
if (name && name.substr(0, 3) === 'new') {
|
||||
this.render_new_doc(doctype, name, doctype_layout);
|
||||
} else {
|
||||
frappe.show_not_found(route);
|
||||
frappe.show_not_found();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -605,7 +605,7 @@ frappe.provide("frappe.views");
|
|||
function make_dom() {
|
||||
var opts = {
|
||||
name: card.name,
|
||||
title: remove_img_tags(card.title),
|
||||
title: frappe.utils.html2text(card.title),
|
||||
disable_click: card._disable_click ? 'disable-click' : '',
|
||||
creation: card.creation,
|
||||
image_url: cur_list.get_image_url(card),
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ frappe.views.Page = class Page {
|
|||
return;
|
||||
}
|
||||
this.wrapper = frappe.container.add_page(this.name);
|
||||
this.wrapper.label = this.pagedoc.title || this.pagedoc.name;
|
||||
this.wrapper.page_name = this.pagedoc.name;
|
||||
|
||||
// set content, script and style
|
||||
|
|
|
|||
|
|
@ -1203,11 +1203,11 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
// Rerender the reports dropdown,
|
||||
// so that this report is included in the dropdown as well.
|
||||
frappe.boot.user.all_reports[r.message] = {
|
||||
ref_doctype: "Item",
|
||||
ref_doctype: this.doctype,
|
||||
report_type: "Report Builder",
|
||||
title: r.message,
|
||||
};
|
||||
this.list_sidebar.setup_reports();
|
||||
|
||||
frappe.set_route('List', this.doctype, 'Report', r.message);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -78,8 +78,9 @@
|
|||
|
||||
.btn.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--white);
|
||||
white-space: nowrap;
|
||||
--icon-stroke: white;
|
||||
--icon-stroke: currentColor;
|
||||
--icon-fill-bg: var(--primary-color);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ textarea.form-control {
|
|||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.frappe-control[data-fieldtype="Data"] .control-input {
|
||||
.frappe-control[data-fieldtype="Data"] .control-input, .control-value {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,14 @@
|
|||
|
||||
font-family: inherit;
|
||||
z-index: 9999 !important;
|
||||
background: var(--fg-color);
|
||||
color: var(--text-color);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
&--nav {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
&--time-current-hours, &--time-current-minutes, &--time-current-seconds {
|
||||
font-family: inherit;
|
||||
|
|
@ -45,6 +53,10 @@
|
|||
|
||||
}
|
||||
|
||||
&--time, &--buttons {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
&--time-row {
|
||||
background-image: linear-gradient(to right, #0089FF, #0089FF);
|
||||
background-repeat: no-repeat;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
--checkbox-right-margin: 8px;
|
||||
|
||||
.label-area {
|
||||
line-height: 1;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
font-size: var(--text-md);
|
||||
margin-right: 10px;
|
||||
&:before {
|
||||
content: url('/assets/frappe/icons/timeless/icon-right-arrow.svg');
|
||||
content: var(--right-arrow-svg);
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ $input-height: 28px !default;
|
|||
|
||||
// input
|
||||
--input-height: #{$input-height};
|
||||
--input-disabled-bg: var(--gray-200);
|
||||
|
||||
// timeline
|
||||
--timeline-item-icon-size: 34px;
|
||||
|
|
@ -60,4 +61,6 @@ $input-height: 28px !default;
|
|||
|
||||
// skeleton
|
||||
--skeleton-bg: var(--gray-100);
|
||||
|
||||
--right-arrow-svg: url("data: image/svg+xml;utf8, <svg width='6' height='8' viewBox='0 0 6 8' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M1.25 7.5L4.75 4L1.25 0.5' stroke='%231F272E' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@
|
|||
--highlight-color: var(--gray-700);
|
||||
--yellow-highlight-color: var(--yellow-700);
|
||||
|
||||
// input
|
||||
--input-disabled-bg: none;
|
||||
|
||||
.frappe-card {
|
||||
.btn-default {
|
||||
background-color: var(--bg-color);
|
||||
|
|
@ -142,4 +145,6 @@
|
|||
|
||||
// skeleton
|
||||
--skeleton-bg: var(--gray-800);
|
||||
|
||||
--right-arrow-svg: url("data: image/svg+xml;utf8, <svg width='6' height='8' viewBox='0 0 6 8' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M1.25 7.5L4.75 4L1.25 0.5' stroke='white' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
--dt-text-color: var(--text-light);
|
||||
--dt-text-light: var(--bg-color);
|
||||
--dt-spacer-1: 0.25rem;
|
||||
--dt-spacer-2: 0.5rem;
|
||||
--dt-spacer-2: var(--padding-xs);
|
||||
--dt-spacer-3: 1rem;
|
||||
--dt-border-radius: var(--border-radius);
|
||||
--dt-cell-bg: var(--fg-color);
|
||||
|
|
@ -26,6 +26,16 @@
|
|||
border-radius: 0px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
height: 100%;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&[data-fieldtype="Select"] .select-icon {
|
||||
top: 5px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.dt-header {
|
||||
|
|
|
|||
|
|
@ -3,19 +3,6 @@ html {
|
|||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
// transition
|
||||
* {
|
||||
transition: background-color 0.5s, background 0.5s;
|
||||
}
|
||||
|
||||
a,
|
||||
.badge {
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.btn {
|
||||
transition: background-color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
|
|
@ -102,7 +89,8 @@ a.badge-hover {
|
|||
}
|
||||
|
||||
pre {
|
||||
color: var(--text-light)
|
||||
color: var(--text-light);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.col-xs-1 { @extend .col-1; }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
@import "variables";
|
||||
@import "css_variables";
|
||||
@import "../common/mixins.scss";
|
||||
@import "../common/global.scss";
|
||||
@import "../common/icons.scss";
|
||||
|
|
|
|||
|
|
@ -60,11 +60,12 @@ $link-color: var(--text-color);
|
|||
// input
|
||||
$input-bg: var(--control-bg);
|
||||
$input-placeholder-color: var(--gray-500);
|
||||
$input-disabled-bg: var(--gray-200);
|
||||
$input-disabled-bg: var(--input-disabled-bg);
|
||||
$input-color: var(--text-color);
|
||||
$input-box-shadow: none;
|
||||
$input-focus-border-color: var(--gray-500);
|
||||
$input-border-radius: var(--border-radius);
|
||||
$input-btn-focus-width: 2px;
|
||||
|
||||
// dropdown
|
||||
$dropdown-color: var(--text-color);
|
||||
|
|
@ -94,6 +95,10 @@ $btn-active-box-shadow: var(--shadow-inset);
|
|||
$mark-bg: #FDF9AF;
|
||||
$mark-padding: 0;
|
||||
|
||||
// transitions
|
||||
$btn-transition: none;
|
||||
$input-transition: none;
|
||||
|
||||
// popover
|
||||
$enable-shadows: true;
|
||||
$popover-border-radius: var(--border-radius);
|
||||
|
|
@ -134,9 +139,9 @@ $grid-breakpoints: (
|
|||
2xl: 1440px
|
||||
) !default;
|
||||
|
||||
@import 'dark';
|
||||
@import 'typography';
|
||||
@import '~bootstrap/scss/functions';
|
||||
@import '~bootstrap/scss/variables';
|
||||
@import "~bootstrap/scss/mixins";
|
||||
@import 'css_variables';
|
||||
@import 'dark';
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ $navbar-height-lg: 4.5rem;
|
|||
|
||||
h2 {
|
||||
font-size: $font-size-2xl;
|
||||
font-weight: 400;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h3 {
|
||||
|
|
@ -250,4 +250,4 @@ $navbar-height-lg: 4.5rem;
|
|||
.breadcrumb {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
@import '~quill/dist/quill.core';
|
||||
@import '~quill/dist/quill.snow.css';
|
||||
@import '~quill/dist/quill.bubble.css';
|
||||
@import 'variables';
|
||||
@import 'css_variables';
|
||||
@import '~bootstrap/scss/bootstrap';
|
||||
@import "../common/mixins";
|
||||
@import "../common/global";
|
||||
|
|
|
|||
|
|
@ -657,6 +657,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.feature-title, .feature-content {
|
||||
|
|
|
|||
|
|
@ -131,5 +131,6 @@ $spacers: (
|
|||
@import "~bootstrap/scss/functions";
|
||||
@import "~bootstrap/scss/variables";
|
||||
@import "~bootstrap/scss/mixins";
|
||||
@import 'css_variables';
|
||||
|
||||
$code-color: $purple;
|
||||
|
|
|
|||
|
|
@ -5,10 +5,14 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from typing import Union, Callable
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
|
||||
def apply():
|
||||
|
|
@ -79,3 +83,43 @@ class RateLimiter:
|
|||
def respond(self):
|
||||
if self.rejected:
|
||||
return Response(_("Too Many Requests"), status=429)
|
||||
|
||||
def rate_limit(key: str, limit: Union[int, Callable] = 5, seconds: int= 24*60*60, methods: Union[str, list]='ALL'):
|
||||
"""Decorator to rate limit an endpoint.
|
||||
|
||||
This will limit Number of requests per endpoint to `limit` within `seconds`.
|
||||
Uses redis cache to track request counts.
|
||||
|
||||
:param key: Key is used to identify the requests uniqueness
|
||||
:param limit: Maximum number of requests to allow with in window time
|
||||
:type limit: Callable or Integer
|
||||
:param seconds: window time to allow requests
|
||||
:param methods: Limit the validation for these methods.
|
||||
`ALL` is a wildcard that applies rate limit on all methods.
|
||||
:type methods: string or list or tuple
|
||||
|
||||
:returns: a decorator function that limit the number of requests per endpoint
|
||||
"""
|
||||
def ratelimit_decorator(fun):
|
||||
@wraps(fun)
|
||||
def wrapper(*args, **kwargs):
|
||||
# Do not apply rate limits if method is not opted to check
|
||||
if methods != 'ALL' and frappe.request.method.upper() not in methods:
|
||||
return frappe.call(fun, **frappe.form_dict)
|
||||
|
||||
_limit = limit() if callable(limit) else limit
|
||||
|
||||
identity = frappe.form_dict[key]
|
||||
cache_key = f"rl:{frappe.form_dict.cmd}:{identity}"
|
||||
|
||||
value = frappe.cache().get_value(cache_key, expires=True) or 0
|
||||
if not value:
|
||||
frappe.cache().set_value(cache_key, 0, expires_in_sec=seconds)
|
||||
|
||||
value = frappe.cache().incrby(cache_key, 1)
|
||||
if value > _limit:
|
||||
frappe.throw(_("You hit the rate limit because of too many requests. Please try after sometime."))
|
||||
|
||||
return frappe.call(fun, **frappe.form_dict)
|
||||
return wrapper
|
||||
return ratelimit_decorator
|
||||
|
|
|
|||
|
|
@ -319,7 +319,10 @@ def send_summary(timespan):
|
|||
|
||||
from_date = getdate(from_date)
|
||||
to_date = getdate()
|
||||
all_users = [user.email for user in get_enabled_system_users()]
|
||||
|
||||
# select only those users that have energy point email notifications enabled
|
||||
all_users = [user.email for user in get_enabled_system_users() if
|
||||
is_email_notifications_enabled_for_type(user.name, 'Energy Point')]
|
||||
|
||||
frappe.sendmail(
|
||||
subject = '{} energy points summary'.format(timespan),
|
||||
|
|
|
|||
33
frappe/tests/test_background_jobs.py
Normal file
33
frappe/tests/test_background_jobs.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import unittest
|
||||
|
||||
from rq import Queue
|
||||
|
||||
import frappe
|
||||
from frappe.core.page.background_jobs.background_jobs import remove_failed_jobs
|
||||
from frappe.utils.background_jobs import get_redis_conn
|
||||
import time
|
||||
|
||||
|
||||
class TestBackgroundJobs(unittest.TestCase):
|
||||
def test_remove_failed_jobs(self):
|
||||
frappe.enqueue(method="frappe.tests.test_background_jobs.fail_function", queue="short")
|
||||
# wait for enqueued job to execute
|
||||
time.sleep(2)
|
||||
conn = get_redis_conn()
|
||||
queues = Queue.all(conn)
|
||||
|
||||
for queue in queues:
|
||||
if queue.name == "short":
|
||||
fail_registry = queue.failed_job_registry
|
||||
self.assertGreater(fail_registry.count, 0)
|
||||
|
||||
remove_failed_jobs()
|
||||
|
||||
for queue in queues:
|
||||
if queue.name == "short":
|
||||
fail_registry = queue.failed_job_registry
|
||||
self.assertEqual(fail_registry.count, 0)
|
||||
|
||||
|
||||
def fail_function():
|
||||
return 1 / 0
|
||||
|
|
@ -4,13 +4,15 @@
|
|||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest
|
||||
import frappe
|
||||
from werkzeug.wrappers import Response
|
||||
import time
|
||||
|
||||
import frappe
|
||||
import frappe.rate_limiter
|
||||
from frappe.rate_limiter import RateLimiter
|
||||
from frappe.utils import cint
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
|
||||
class TestRateLimiter(unittest.TestCase):
|
||||
|
|
|
|||
34
frappe/utils/lazy_loader.py
Normal file
34
frappe/utils/lazy_loader.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import importlib.util
|
||||
import sys
|
||||
|
||||
def lazy_import(name, package=None):
|
||||
"""Import a module lazily.
|
||||
|
||||
The module is loaded when modules's attribute is accessed for the first time.
|
||||
This works with both absolute and relative imports.
|
||||
$ cat mod.py
|
||||
print("Loading mod.py")
|
||||
$ python -i lazy_loader.py
|
||||
>>> mod = lazy_import("mod") # Module is not loaded
|
||||
>>> mod.__str__() # module is loaded on accessing attribute
|
||||
Loading mod.py
|
||||
"<module 'mod' from '.../frappe/utils/mod.py'>"
|
||||
>>>
|
||||
|
||||
Code based on https://github.com/python/cpython/blob/master/Doc/library/importlib.rst#implementing-lazy-imports.
|
||||
"""
|
||||
# Return if the module already loaded
|
||||
if name in sys.modules:
|
||||
return sys.modules[name]
|
||||
|
||||
# Find the spec if not loaded
|
||||
spec = importlib.util.find_spec(name, package)
|
||||
if not spec:
|
||||
raise ImportError(f'Module {name} Not found.')
|
||||
|
||||
loader = importlib.util.LazyLoader(spec.loader)
|
||||
spec.loader = loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
loader.exec_module(module)
|
||||
return module
|
||||
|
|
@ -231,14 +231,14 @@ def update_oauth_user(user, data, provider):
|
|||
save = True
|
||||
user = frappe.new_doc("User")
|
||||
|
||||
gender = (data.get("gender") or "").title()
|
||||
gender = data.get("gender", "").title()
|
||||
|
||||
if not frappe.db.exists("Gender", gender):
|
||||
if gender and not frappe.db.exists("Gender", gender):
|
||||
doc = frappe.new_doc("Gender", {"gender": gender})
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
user.update({
|
||||
"doctype":"User",
|
||||
"doctype": "User",
|
||||
"first_name": get_first_name(data),
|
||||
"last_name": get_last_name(data),
|
||||
"email": get_email(data),
|
||||
|
|
|
|||
|
|
@ -90,14 +90,6 @@ def delete_login_failed_cache(user):
|
|||
frappe.cache().hdel('login_failed_count', user)
|
||||
frappe.cache().hdel('locked_account_time', user)
|
||||
|
||||
|
||||
def delete_password_reset_cache(user=None):
|
||||
if user:
|
||||
frappe.cache().hdel('password_reset_link_count', user)
|
||||
else:
|
||||
frappe.cache().delete_key('password_reset_link_count')
|
||||
|
||||
|
||||
def update_password(user, pwd, doctype='User', fieldname='password', logout_all_sessions=False):
|
||||
'''
|
||||
Update the password for the User
|
||||
|
|
@ -179,3 +171,6 @@ def get_encryption_key():
|
|||
frappe.local.conf.encryption_key = encryption_key
|
||||
|
||||
return frappe.local.conf.encryption_key
|
||||
|
||||
def get_password_reset_limit():
|
||||
return frappe.db.get_single_value("System Settings", "password_reset_limit") or 0
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@
|
|||
"idx": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-06 19:17:46.083764",
|
||||
"modified": "2021-03-02 17:42:32.947404",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Contact Us Settings",
|
||||
|
|
@ -138,5 +138,6 @@
|
|||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
</div>
|
||||
<div>
|
||||
{%- if feature.url -%}
|
||||
<a href="{{ feature.url }}" class="feature-url">Learn more →</a>
|
||||
<a href="{{ feature.url }}" class="feature-url stretched-link">Learn more →</a>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,65 +1,67 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<!-- Chrome, Firefox OS and Opera -->
|
||||
<meta name="theme-color" content="#0089FF">
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#0089FF">
|
||||
<!-- iOS Safari -->
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="#0089FF">
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
||||
<meta content="utf-8" http-equiv="encoding">
|
||||
<meta name="author" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0,
|
||||
maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, minimal-ui">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="white">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<title>Frappe</title>
|
||||
<link rel="shortcut icon"
|
||||
href="{{ favicon or "/assets/frappe/images/frappe-favicon.svg" }}" type="image/x-icon">
|
||||
<link rel="icon"
|
||||
href="{{ favicon or "/assets/frappe/images/frappe-favicon.svg" }}" type="image/x-icon">
|
||||
{% for include in include_css -%}
|
||||
<link type="text/css" rel="stylesheet" href="{{ include }}?ver={{ build_version }}">
|
||||
{%- endfor -%}
|
||||
</head>
|
||||
<body data-theme="{{ desk_theme.lower() }}">
|
||||
{% include "public/icons/timeless/symbol-defs.svg" %}
|
||||
<div class="centered splash">
|
||||
<img src="{{ splash_image or "/assets/frappe/images/frappe-framework-logo.png" }}"
|
||||
style="max-width: 100px; max-height: 100px;">
|
||||
</div>
|
||||
<div class="main-section">
|
||||
<header></header>
|
||||
<div id="body"></div>
|
||||
<footer></footer>
|
||||
</div>
|
||||
<html data-theme="{{ desk_theme.lower() }}">
|
||||
<head>
|
||||
<!-- Chrome, Firefox OS and Opera -->
|
||||
<meta name="theme-color" content="#0089FF">
|
||||
<!-- Windows Phone -->
|
||||
<meta name="msapplication-navbutton-color" content="#0089FF">
|
||||
<!-- iOS Safari -->
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="#0089FF">
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
||||
<meta content="utf-8" http-equiv="encoding">
|
||||
<meta name="author" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0,
|
||||
maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, minimal-ui">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="white">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<title>Frappe</title>
|
||||
<link rel="shortcut icon"
|
||||
href="{{ favicon or "/assets/frappe/images/frappe-favicon.svg" }}" type="image/x-icon">
|
||||
<link rel="icon"
|
||||
href="{{ favicon or "/assets/frappe/images/frappe-favicon.svg" }}" type="image/x-icon">
|
||||
{% for include in include_css -%}
|
||||
<link type="text/css" rel="stylesheet" href="{{ include }}?ver={{ build_version }}">
|
||||
{%- endfor -%}
|
||||
</head>
|
||||
<body>
|
||||
{% include "public/icons/timeless/symbol-defs.svg" %}
|
||||
<div class="centered splash">
|
||||
<img src="{{ splash_image or "/assets/frappe/images/frappe-framework-logo.png" }}"
|
||||
style="max-width: 100px; max-height: 100px;">
|
||||
</div>
|
||||
<div class="main-section">
|
||||
<header></header>
|
||||
<div id="body"></div>
|
||||
<footer></footer>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="/assets/frappe/js/lib/jquery/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="/assets/frappe/js/lib/jquery/jquery.min.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
window._version_number = "{{ build_version }}";
|
||||
// browser support
|
||||
window.app = true;
|
||||
window.dev_server = {{ dev_server }};
|
||||
<script type="text/javascript">
|
||||
window._version_number = "{{ build_version }}";
|
||||
// browser support
|
||||
window.app = true;
|
||||
window.dev_server = {{ dev_server }};
|
||||
|
||||
if(!window.frappe) window.frappe = {};
|
||||
if(!window.frappe) window.frappe = {};
|
||||
|
||||
frappe.boot = {{ boot }};
|
||||
frappe.boot = {{ boot }};
|
||||
|
||||
frappe.csrf_token = "{{ csrf_token }}";
|
||||
frappe.csrf_token = "{{ csrf_token }}";
|
||||
|
||||
</script>
|
||||
</script>
|
||||
|
||||
{% for include in include_js %}
|
||||
<script type="text/javascript" src="{{ include }}?ver={{ build_version }}"></script>
|
||||
{% endfor %}
|
||||
{% include "templates/includes/app_analytics/google_analytics.html" %}
|
||||
{% include "templates/includes/app_analytics/mixpanel_analytics.html" %}
|
||||
{% for include in include_js %}
|
||||
<script type="text/javascript" src="{{ include }}?ver={{ build_version }}"></script>
|
||||
{% endfor %}
|
||||
{% include "templates/includes/app_analytics/google_analytics.html" %}
|
||||
{% include "templates/includes/app_analytics/mixpanel_analytics.html" %}
|
||||
|
||||
{% for sound in (sounds or []) %}
|
||||
<audio preload="auto" id="sound-{{ sound.name }}" volume={{ sound.volume or 1 }}>
|
||||
<source src="{{ sound.src }}"></source>
|
||||
</audio>
|
||||
{% endfor %}
|
||||
</body>
|
||||
{% for sound in (sounds or []) %}
|
||||
<audio preload="auto" id="sound-{{ sound.name }}" volume={{ sound.volume or 1 }}>
|
||||
<source src="{{ sound.src }}"></source>
|
||||
</audio>
|
||||
{% endfor %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ passlib==1.7.3
|
|||
pdfkit==0.6.1
|
||||
Pillow>=8.0.0
|
||||
premailer==3.6.1
|
||||
psutil==5.7.2
|
||||
psycopg2-binary==2.8.4
|
||||
pyasn1==0.4.8
|
||||
PyJWT==1.7.1
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue