@@ -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) | |||
@@ -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 |
@@ -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: | |||
@@ -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}" | |||
@@ -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 | |||
} |
@@ -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 *", | |||
@@ -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", | |||
@@ -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 | |||
@@ -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""" | |||
@@ -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("): | |||
@@ -613,8 +613,8 @@ | |||
<symbol viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" id="icon-expenses"> | |||
<path d="M17 16a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V2.37a.2.2 0 0 1 .31-.168l1.75 1.143a.5.5 0 0 0 .547 0L7.393 2.18a.5.5 0 0 1 .547 0l1.787 1.166a.5.5 0 0 0 .546 0L12.06 2.18a.5.5 0 0 1 .547 0l1.786 1.166a.5.5 0 0 0 .547 0l1.75-1.143a.2.2 0 0 1 .31.167V16z" | |||
stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="square"></path> | |||
<path d="M12.925 9.097l-.298 1.052h-.904c-.174 1.15-.997 1.986-2.748 2.093l2.787 3.413v.072h-1.556l-3.05-3.703-.01-.83H8.66c.938 0 1.53-.372 1.705-1.045H7.04l.298-1.052h2.992c-.196-.601-.737-.963-1.67-.963H7.04L7.351 7h5.578l-.302 1.061-1.343-.008c.222.298.367.652.43 1.044h1.211z" | |||
fill="#192734" stroke="none"></path> | |||
<path d="m13.13,8.1l-0.3,1.05l-0.91,0c-0.17,1.15 -0.99,1.98 -2.74,2.09l2.78,3.41l0,0.08l-1.55,0l-3.05,-3.71l-0.01,-0.83l1.51,0c0.94,0 1.53,-0.37 1.71,-1.04l-3.33,0l0.3,-1.05l2.99,0c-0.2,-0.6 -0.74,-0.97 -1.67,-0.97l-1.62,0l0.31,-1.13l5.58,0l-0.3,1.06l-1.35,-0.01c0.23,0.3 0.37,0.66 0.43,1.05l1.22,0z" | |||
fill="var(--icon-stroke)" stroke="none"></path> | |||
</symbol> | |||
<symbol viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" id="icon-income"> | |||
@@ -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 | |||
@@ -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, |
@@ -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): | |||