Merge branch 'develop' of https://github.com/frappe/frappe into double-signature-in-email

This commit is contained in:
pateljannat 2021-03-11 11:17:06 +05:30
commit ca64d50dd4
83 changed files with 707 additions and 409 deletions

22
.github/workflows/semgrep.yml vendored Normal file
View 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
View 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

View file

@ -7,7 +7,7 @@ addons:
- test_site_producer
mariadb: 10.3
postgresql: 9.5
chrome: stable
firefox: latest
services:
- xvfb

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,6 +22,7 @@ class Recorder {
}
show() {
if (!this.view || this.view.$route.name == "recorder-detail") return;
this.view.$router.replace({name: "recorder-detail"});
}
}

View file

@ -76,6 +76,7 @@ frappe.ui.form.on("Customize Form", {
frm.trigger("setup_sortable");
}
}
localStorage["customize_doctype"] = frm.doc.doc_type;
}
});
} else {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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'})

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -131,7 +131,7 @@ frappe.form.formatters = {
return repl('<a onclick="%(onclick)s">%(value)s</a>',
{onclick: docfield.link_onclick.replace(/"/g, '&quot;'), 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}"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -284,7 +284,7 @@ export default {
frappe.breadcrumbs.add({
type: 'Custom',
label: __('Recorder'),
route: '#recorder'
route: '/app/recorder'
});
frappe.call({
method: "frappe.recorder.get",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,6 @@
--checkbox-right-margin: 8px;
.label-area {
line-height: 1;
font-size: var(--text-sm);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,4 @@
@import "variables";
@import "css_variables";
@import "../common/mixins.scss";
@import "../common/global.scss";
@import "../common/icons.scss";

View file

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

View file

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

View file

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

View file

@ -657,6 +657,7 @@
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
}
.feature-title, .feature-content {

View file

@ -131,5 +131,6 @@ $spacers: (
@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins";
@import 'css_variables';
$code-color: $purple;

View file

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

View file

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

View 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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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