diff --git a/.github/helper/roulette.py b/.github/helper/roulette.py
index 9165198012..b859b87047 100644
--- a/.github/helper/roulette.py
+++ b/.github/helper/roulette.py
@@ -77,13 +77,13 @@ if __name__ == "__main__":
updated_py_file_count = len(list(filter(is_py, files_list)))
only_py_changed = updated_py_file_count == len(files_list)
- if ci_files_changed:
- print("CI related files were updated, running all build processes.")
-
- elif has_skip_ci_label(pr_number, repo):
+ if has_skip_ci_label(pr_number, repo):
print("Found `Skip CI` label on pr, stopping build process.")
sys.exit(0)
+ elif ci_files_changed:
+ print("CI related files were updated, running all build processes.")
+
elif only_docs_changed:
print("Only docs were updated, stopping build process.")
sys.exit(0)
diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
index 4c845dba8c..6d1029d51d 100644
--- a/.github/workflows/linters.yml
+++ b/.github/workflows/linters.yml
@@ -11,10 +11,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- - name: Set up Python 3.8
+ - name: Set up Python
uses: actions/setup-python@v4
with:
- python-version: 3.8
+ python-version: '3.10'
- name: Install and Run Pre-commit
uses: pre-commit/action@v3.0.0
@@ -22,10 +22,8 @@ jobs:
- name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
- - uses: returntocorp/semgrep-action@v1
- env:
- SEMGREP_TIMEOUT: 120
- with:
- config: >-
- r/python.lang.correctness
- ./frappe-semgrep-rules/rules
+ - name: Download semgrep
+ run: pip install semgrep==0.97.0
+
+ - name: Run Semgrep rules
+ run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index b4cbbd6233..e56803acb7 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -171,7 +171,7 @@ class DocType(Document):
if docfield.fieldname in method_set:
conflict_type = "controller method"
- if docfield.fieldname in property_set:
+ if docfield.fieldname in property_set and not docfield.is_virtual:
conflict_type = "class property"
if conflict_type:
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index e8b8da76ab..bb4b441680 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -16,7 +16,7 @@ from requests.exceptions import HTTPError, SSLError
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import call_hook_method, cint, get_files_path, get_hook_method
+from frappe.utils import call_hook_method, cint, get_files_path, get_hook_method, get_url
from frappe.utils.file_manager import is_safe_path
from frappe.utils.image import optimize_image, strip_exif_data
@@ -61,7 +61,12 @@ class File(Document):
self.set_file_name()
self.validate_attachment_limit()
- if not self.is_folder and not self.is_remote_file:
+ if self.is_folder:
+ return
+
+ if self.is_remote_file:
+ self.validate_remote_file()
+ else:
self.save_file(content=self.get_content())
self.flags.new_file = True
frappe.local.rollback_observers.append(self)
@@ -255,6 +260,12 @@ class File(Document):
title=_("Attachment Limit Reached"),
)
+ def validate_remote_file(self):
+ """Validates if file uploaded using URL already exist"""
+ site_url = get_url()
+ if "/files/" in self.file_url and self.file_url.startswith(site_url):
+ self.file_url = self.file_url.split(site_url, 1)[1]
+
def set_folder_name(self):
"""Make parent folders if not exists based on reference doctype and name"""
if self.folder:
@@ -445,6 +456,10 @@ class File(Document):
file_path = self.file_url or self.file_name
+ site_url = get_url()
+ if "/files/" in file_path and file_path.startswith(site_url):
+ file_path = file_path.split(site_url, 1)[1]
+
if "/" not in file_path:
if self.is_private:
file_path = f"/private/files/{file_path}"
diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json
index d4d79b21fb..cc2a0e870a 100644
--- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json
+++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json
@@ -16,8 +16,11 @@
"server_script",
"frequency",
"cron_format",
+ "create_log",
+ "status_section",
"last_execution",
- "create_log"
+ "column_break_9",
+ "next_execution"
],
"fields": [
{
@@ -72,6 +75,22 @@
"options": "Server Script",
"read_only": 1,
"search_index": 1
+ },
+ {
+ "fieldname": "next_execution",
+ "fieldtype": "Datetime",
+ "is_virtual": 1,
+ "label": "Next Execution",
+ "read_only": 1
+ },
+ {
+ "fieldname": "status_section",
+ "fieldtype": "Section Break",
+ "label": "Status"
+ },
+ {
+ "fieldname": "column_break_9",
+ "fieldtype": "Column Break"
}
],
"in_create": 1,
@@ -81,7 +100,7 @@
"link_fieldname": "scheduled_job_type"
}
],
- "modified": "2020-10-07 10:39:24.519460",
+ "modified": "2022-06-28 02:55:12.470915",
"modified_by": "Administrator",
"module": "Core",
"name": "Scheduled Job Type",
@@ -103,5 +122,7 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
+ "title_field": "method",
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
index 318b156dcd..673805ae8b 100644
--- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
+++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
@@ -50,6 +50,10 @@ class ScheduledJobType(Document):
queued_jobs = get_jobs(site=frappe.local.site, key="job_type")[frappe.local.site]
return self.method in queued_jobs
+ @property
+ def next_execution(self):
+ return self.get_next_execution()
+
def get_next_execution(self):
CRON_MAP = {
"Yearly": "0 0 1 1 *",
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index c954e41202..a444062b5a 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -63,6 +63,7 @@
"otp_issuer_name",
"email",
"email_footer_address",
+ "email_retry_limit",
"column_break_18",
"disable_standard_email_footer",
"hide_footer_in_auto_email_reports",
@@ -495,8 +496,8 @@
"fieldname": "allow_older_web_view_links",
"fieldtype": "Check",
"label": "Allow Older Web View Links (Insecure)"
- },
- {
+ },
+ {
"fieldname": "column_break_64",
"fieldtype": "Column Break"
},
@@ -518,12 +519,18 @@
"fieldtype": "Duration",
"label": "Reset Password Link Expiry Duration",
"non_negative": 1
+ },
+ {
+ "default": "3",
+ "fieldname": "email_retry_limit",
+ "fieldtype": "Int",
+ "label": "Email Retry Limit"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2022-05-19 00:00:18.095269",
+ "modified": "2022-06-21 13:55:04.796152",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py
index d6dce68399..893c2a6606 100644
--- a/frappe/desk/reportview.py
+++ b/frappe/desk/reportview.py
@@ -156,8 +156,6 @@ def setup_group_by(data):
**data
)
)
- if data.aggregate_on_field:
- data.fields.append(f"`tab{data.aggregate_on_doctype}`.`{data.aggregate_on_field}`")
else:
raise_invalid_field(data.aggregate_on_field)
@@ -435,11 +433,20 @@ def append_totals_row(data):
def get_labels(fields, doctype):
"""get column labels based on column names"""
labels = []
+ doctype = doctype.lower()
for key in fields:
- key = key.split(" as ")[0]
+ aggregate_function = ""
+
+ key = key.casefold().split(" as ", maxsplit=1)[0]
if key.startswith(("count(", "sum(", "avg(")):
- continue
+ # Get aggregate function and _aggregate_column
+ # key = 'sum(`tabDocType`.`fieldname`)'
+ if not key.rstrip().endswith(")"):
+ continue
+ _agg_fn, _key = key.split("(", maxsplit=1)
+ aggregate_function = _agg_fn.lower() # aggregate_function = 'sum'
+ key = _key[:-1] # key = `tabDocType`.`fieldname`
if "." in key:
parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`")
@@ -455,7 +462,10 @@ def get_labels(fields, doctype):
if parenttype != doctype:
# If the column is from a child table, append the child doctype.
# For example, "Item Code (Sales Invoice Item)".
- label += f" ({ _(parenttype) })"
+ label += f" ({ _(parenttype.title()) })"
+
+ if aggregate_function:
+ label = _("{0} of {1}").format(aggregate_function.capitalize(), label)
labels.append(label)
@@ -464,7 +474,7 @@ def get_labels(fields, doctype):
def handle_duration_fieldtype_values(doctype, data, fields):
for field in fields:
- key = field.split(" as ")[0]
+ key = field.casefold().split(" as ", maxsplit=1)[0]
if key.startswith(("count(", "sum(", "avg(")):
continue
diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py
index c3002607b4..4fc828eb62 100644
--- a/frappe/email/doctype/email_queue/email_queue.py
+++ b/frappe/email/doctype/email_queue/email_queue.py
@@ -30,8 +30,6 @@ from frappe.utils import (
split_emails,
)
-MAX_RETRY_COUNT = 3
-
class EmailQueue(Document):
DOCTYPE = "Email Queue"
@@ -210,7 +208,7 @@ class SendMailContext:
email_status = (self.sent_to and "Partially Sent") or "Not Sent"
self.queue_doc.update_status(status=email_status, commit=True)
elif exc_type:
- if self.queue_doc.retry < MAX_RETRY_COUNT:
+ if self.queue_doc.retry < get_email_retry_limit():
update_fields = {"status": "Not Sent", "retry": self.queue_doc.retry + 1}
else:
update_fields = {"status": (self.sent_to and "Partially Errored") or "Error"}
@@ -372,6 +370,10 @@ def on_doctype_update():
)
+def get_email_retry_limit():
+ return cint(frappe.db.get_system_setting("email_retry_limit")) or 3
+
+
class QueueBuilder:
"""Builds Email Queue from the given data"""
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index 7fb38848e2..0b8f790246 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -417,6 +417,8 @@ class DatabaseQuery(object):
"extract(",
"locate(",
"strpos(",
+ ]
+ aggregate_functions = [
"count(",
"sum(",
"avg(",
@@ -427,6 +429,9 @@ class DatabaseQuery(object):
if not ("tab" in field and "." in field) or any(x for x in sql_functions if x in field):
continue
+ if any(x for x in aggregate_functions if x in field):
+ field = field.split("(", 1)[1][:-1]
+
table_name = field.split(".")[0]
if table_name.lower().startswith("group_concat("):
diff --git a/frappe/public/icons/timeless/icons.svg b/frappe/public/icons/timeless/icons.svg
index fbd72d6fb5..af63d0c8ff 100644
--- a/frappe/public/icons/timeless/icons.svg
+++ b/frappe/public/icons/timeless/icons.svg
@@ -613,8 +613,8 @@
-
+
diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py
index c1b2e05266..2dd1743767 100644
--- a/frappe/tests/test_db_query.py
+++ b/frappe/tests/test_db_query.py
@@ -643,9 +643,7 @@ class TestReportview(unittest.TestCase):
)
response = execute_cmd("frappe.desk.reportview.get")
- self.assertListEqual(
- response["keys"], ["field_label", "field_name", "_aggregate_column", "columns"]
- )
+ self.assertListEqual(response["keys"], ["field_label", "field_name", "_aggregate_column"])
def test_cast_name(self):
from frappe.core.doctype.doctype.test_doctype import new_doctype
diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv
index d00548458c..b22befd321 100644
--- a/frappe/translations/de.csv
+++ b/frappe/translations/de.csv
@@ -1993,7 +1993,7 @@ QR Code,QR-Code,
QR Code for Login Verification,QR Code für Login-Bestätigung,
QZ Tray Connection Active!,QZ-Tray-Verbindung aktiv!,
QZ Tray Failed: ,QZ-Fach fehlgeschlagen:,
-Quarter Day,Quartalstag,
+Quarter Day,Viertel-Tag,
Query,Abfrage,
Query Report,Abfragebericht,
Query must be a SELECT,Abfrage muss ein SELECT sein,
@@ -4793,3 +4793,17 @@ Reset to default,Auf Standard zurücksetzen,
Column Width,Spaltenbreite,
Choose Kanban Board,Kanban-Tafel auswählen,
Create New Board,Neue Tafel erstellen,
+Only If Creator,Nur wenn Ersteller,
+Rebuild Tree,Baum neu aufbauen,
+Customize Dashboard,Dashboard anpassen,
+Reset Dashboard Customizations,Dashboard-Anpassungen zurücksetzen,
+Add {0},{0} hinzufügen,
+descending,absteigend,
+ascending,aufsteigend,
+Next Document,Nächstes Dokument,
+Previous Document,Vorheriges Dokument,
+Mark all as read,Alle als gelesen markieren,
+See all Activity,Alle Aktivitäten anzeigen,
+Go to Notification Settings List,Gehe zur Listenansicht Benachrichtigungseinstellungen,
+Load more,Mehr laden,
+Edit Full Form,Vollständiges Formular bearbeiten,
diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py
index 178c3d4416..c29836af32 100644
--- a/frappe/utils/redis_wrapper.py
+++ b/frappe/utils/redis_wrapper.py
@@ -199,7 +199,7 @@ class RedisWrapper(redis.Redis):
frappe.local.cache[_name][key] = value
elif generator:
value = generator()
- self.hset(name, key, value)
+ self.hset(name, key, value, shared=shared)
return value
def hdel(self, name, key, shared=False):