@@ -740,17 +740,26 @@ def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=Fals | |||
:param doc: [optional] Checks User permissions for given doc. | |||
:param user: [optional] Check for given user. Default: current user. | |||
:param parent_doctype: Required when checking permission for a child DocType (unless doc is specified).""" | |||
import frappe.permissions | |||
if not doctype and doc: | |||
doctype = doc.doctype | |||
import frappe.permissions | |||
out = frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user, | |||
raise_exception=throw, parent_doctype=parent_doctype) | |||
if throw and not out: | |||
if doc: | |||
frappe.throw(_("No permission for {0}").format(doc.doctype + " " + doc.name)) | |||
else: | |||
frappe.throw(_("No permission for {0}").format(doctype)) | |||
# mimics frappe.throw | |||
document_label = f"{doc.doctype} {doc.name}" if doc else doctype | |||
msgprint( | |||
_("No permission for {0}").format(document_label), | |||
raise_exception=ValidationError, | |||
title=None, | |||
indicator='red', | |||
is_minimizable=None, | |||
wide=None, | |||
as_list=False | |||
) | |||
return out | |||
@@ -32,6 +32,7 @@ def get_list(doctype, fields=None, filters=None, order_by=None, | |||
args = frappe._dict( | |||
doctype=doctype, | |||
parent_doctype=parent, | |||
fields=fields, | |||
filters=filters, | |||
or_filters=or_filters, | |||
@@ -29,6 +29,7 @@ from frappe import _, conf, safe_decode | |||
from frappe.model.document import Document | |||
from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, get_hook_method, random_string, strip | |||
from frappe.utils.image import strip_exif_data, optimize_image | |||
from frappe.utils.file_manager import safe_b64decode | |||
class MaxFileSizeReachedError(frappe.ValidationError): | |||
pass | |||
@@ -436,7 +437,7 @@ class File(Document): | |||
if b"," in self.content: | |||
self.content = self.content.split(b",")[1] | |||
self.content = base64.b64decode(self.content) | |||
self.content = safe_b64decode(self.content) | |||
if not self.is_private: | |||
self.is_private = 0 | |||
@@ -852,7 +853,7 @@ def extract_images_from_html(doc, content, is_private=False): | |||
content = content.encode("utf-8") | |||
if b"," in content: | |||
content = content.split(b",")[1] | |||
content = base64.b64decode(content) | |||
content = safe_b64decode(content) | |||
content = optimize_image(content, mtype) | |||
@@ -1,4 +1,4 @@ | |||
// Copyright (c) 2016, {app_publisher} and contributors | |||
// Copyright (c) {year}, {app_publisher} and contributors | |||
// For license information, please see license.txt | |||
/* eslint-disable */ | |||
@@ -1,5 +1,5 @@ | |||
# Copyright (c) 2013, {app_publisher} and contributors | |||
# License: MIT. See LICENSE | |||
# Copyright (c) {year}, {app_publisher} and contributors | |||
# For license information, please see license.txt | |||
# import frappe | |||
@@ -135,9 +135,10 @@ class MariaDBDatabase(Database): | |||
table_name = get_table_name(doctype) | |||
return self.sql(f"DESC `{table_name}`") | |||
def change_column_type(self, doctype: str, column: str, type: str) -> Union[List, Tuple]: | |||
def change_column_type(self, doctype: str, column: str, type: str, nullable: bool = False) -> Union[List, Tuple]: | |||
table_name = get_table_name(doctype) | |||
return self.sql(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} NOT NULL") | |||
null_constraint = "NOT NULL" if not nullable else "" | |||
return self.sql(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} {null_constraint}") | |||
# exception types | |||
@staticmethod | |||
@@ -183,9 +183,12 @@ class PostgresDatabase(Database): | |||
table_name = get_table_name(doctype) | |||
return self.sql(f"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = '{table_name}'") | |||
def change_column_type(self, doctype: str, column: str, type: str) -> Union[List, Tuple]: | |||
def change_column_type(self, doctype: str, column: str, type: str, nullable: bool = False) -> Union[List, Tuple]: | |||
table_name = get_table_name(doctype) | |||
return self.sql(f'ALTER TABLE "{table_name}" ALTER COLUMN "{column}" TYPE {type}') | |||
null_constraint = "SET NOT NULL" if not nullable else "DROP NOT NULL" | |||
return self.sql(f"""ALTER TABLE "{table_name}" | |||
ALTER COLUMN "{column}" TYPE {type}, | |||
ALTER COLUMN "{column}" {null_constraint}""") | |||
def create_auth_table(self): | |||
self.sql_ddl("""create table if not exists "__Auth" ( | |||
@@ -382,10 +382,10 @@ class Leaderboard { | |||
let timespan = this.options.selected_timespan.toLowerCase(); | |||
let current_date = frappe.datetime.now_date(); | |||
let date_range_map = { | |||
"this week": [frappe.datetime.week_start(), current_date], | |||
"this month": [frappe.datetime.month_start(), current_date], | |||
"this quarter": [frappe.datetime.quarter_start(), current_date], | |||
"this year": [frappe.datetime.year_start(), current_date], | |||
"this week": [frappe.datetime.week_start(), frappe.datetime.week_end()], | |||
"this month": [frappe.datetime.month_start(), frappe.datetime.month_end()], | |||
"this quarter": [frappe.datetime.quarter_start(), frappe.datetime.quarter_end()], | |||
"this year": [frappe.datetime.year_start(), frappe.datetime.year_end()], | |||
"last week": [frappe.datetime.add_days(current_date, -7), current_date], | |||
"last month": [frappe.datetime.add_months(current_date, -1), current_date], | |||
"last quarter": [frappe.datetime.add_months(current_date, -3), current_date], | |||
@@ -36,10 +36,12 @@ class DatabaseQuery(object): | |||
ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False, | |||
update=None, add_total_row=None, user_settings=None, reference_doctype=None, | |||
run=True, strict=True, pluck=None, ignore_ddl=False, parent_doctype=None) -> List: | |||
if not ignore_permissions and \ | |||
not frappe.has_permission(self.doctype, "select", user=user, parent_doctype=parent_doctype) and \ | |||
not frappe.has_permission(self.doctype, "read", user=user, parent_doctype=parent_doctype): | |||
if ( | |||
not ignore_permissions | |||
and not frappe.has_permission(self.doctype, "select", user=user, parent_doctype=parent_doctype) | |||
and not frappe.has_permission(self.doctype, "read", user=user, parent_doctype=parent_doctype) | |||
): | |||
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype)) | |||
raise frappe.PermissionError(self.doctype) | |||
@@ -787,12 +789,15 @@ class DatabaseQuery(object): | |||
def check_parent_permission(parent, child_doctype): | |||
if parent: | |||
# User may pass fake parent and get the information from the child table | |||
if child_doctype and not frappe.db.exists('DocField', | |||
{'parent': parent, 'options': child_doctype}): | |||
if child_doctype and not ( | |||
frappe.db.exists('DocField', {'parent': parent, 'options': child_doctype}) | |||
or frappe.db.exists('Custom Field', {'dt': parent, 'options': child_doctype}) | |||
): | |||
raise frappe.PermissionError | |||
if frappe.permissions.has_permission(parent): | |||
return | |||
# Either parent not passed or the user doesn't have permission on parent doctype of child table! | |||
raise frappe.PermissionError | |||
@@ -188,5 +188,5 @@ frappe.patches.v14_0.copy_mail_data #08.03.21 | |||
frappe.patches.v14_0.update_workspace2 # 20.09.2021 | |||
frappe.patches.v14_0.update_github_endpoints #08-11-2021 | |||
frappe.patches.v14_0.remove_db_aggregation | |||
frappe.patches.v14_0.save_ratings_in_fraction | |||
frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021 | |||
frappe.patches.v14_0.update_color_names_in_kanban_board_column |
@@ -1,12 +1,39 @@ | |||
import frappe | |||
from frappe.query_builder import DocType | |||
def execute(): | |||
rating_fields = frappe.get_all("DocField", fields=["parent", "fieldname"], filters={"fieldtype": "Rating"}) | |||
RATING_FIELD_TYPE = "decimal(3,2)" | |||
rating_fields = frappe.get_all( | |||
"DocField", fields=["parent", "fieldname"], filters={"fieldtype": "Rating"} | |||
) | |||
custom_rating_fields = frappe.get_all( | |||
"Custom Field", fields=["dt", "fieldname"], filters={"fieldtype": "Rating"} | |||
) | |||
for _field in rating_fields + custom_rating_fields: | |||
doctype_name = _field.get("parent") or _field.get("dt") | |||
doctype = DocType(doctype_name) | |||
field = _field.fieldname | |||
# TODO: Add postgres support (for the check) | |||
if ( | |||
frappe.conf.db_type == "mariadb" | |||
and frappe.db.get_column_type(doctype_name, field) == RATING_FIELD_TYPE | |||
): | |||
continue | |||
# commit any changes so far for upcoming DDL | |||
frappe.db.commit() | |||
# alter column types for rating fieldtype | |||
frappe.db.change_column_type(doctype_name, column=field, type=RATING_FIELD_TYPE, nullable=True) | |||
custom_rating_fields = frappe.get_all("Custom Field", fields=["dt", "fieldname"], filters={"fieldtype": "Rating"}) | |||
# update data: int => decimal | |||
frappe.qb.update(doctype).set( | |||
doctype[field], doctype[field] / 5 | |||
).run() | |||
for field in rating_fields + custom_rating_fields: | |||
doctype_name = field.get("parent") or field.get("dt") | |||
doctype = frappe.qb.DocType(doctype_name) | |||
field = field.fieldname | |||
(frappe.qb.update(doctype_name).set(doctype[field], doctype[field]/5)).run() | |||
# commit to flush updated rows | |||
frappe.db.commit() |
@@ -190,7 +190,7 @@ frappe.ui.form.Form = class FrappeForm { | |||
setup_std_layout() { | |||
this.form_wrapper = $('<div></div>').appendTo(this.layout_main); | |||
this.body = $('<div></div>').appendTo(this.form_wrapper); | |||
this.body = $('<div class="std-form-layout"></div>').appendTo(this.form_wrapper); | |||
// only tray | |||
this.meta.section_style='Simple'; // always simple! | |||
@@ -211,12 +211,24 @@ frappe.ui.form.Form = class FrappeForm { | |||
this.fields = this.layout.fields_list; | |||
let dashboard_parent = $('<div class="form-dashboard">'); | |||
let dashboard_added = false; | |||
if (this.layout.tabs.length) { | |||
this.layout.tabs[0].wrapper.prepend(dashboard_parent); | |||
this.layout.tabs.every(tab => { | |||
if (tab.df.options === 'Dashboard') { | |||
tab.wrapper.prepend(dashboard_parent); | |||
dashboard_added = true; | |||
return false; | |||
} | |||
return true; | |||
}); | |||
if (!dashboard_added) { | |||
this.layout.tabs[0].wrapper.prepend(dashboard_parent); | |||
} | |||
} else { | |||
dashboard_parent.insertAfter(this.layout.wrapper.find('.form-message')); | |||
this.layout.wrapper.find('.form-page').prepend(dashboard_parent); | |||
} | |||
this.dashboard = new frappe.ui.form.Dashboard(dashboard_parent, this); | |||
this.tour = new frappe.ui.form.FormTour({ | |||
@@ -29,7 +29,7 @@ $.extend(frappe.contacts, { | |||
} | |||
}, | |||
get_last_doc: function(frm) { | |||
const reverse_routes = frappe.route_history.reverse(); | |||
const reverse_routes = frappe.route_history.slice().reverse(); | |||
const last_route = reverse_routes.find(route => { | |||
return route[0] === 'Form' && route[1] !== frm.doctype | |||
}) | |||
@@ -1,4 +1,5 @@ | |||
.form-control { | |||
height: inherit; | |||
border: none; | |||
font-size: var(--text-md); | |||
position: relative; | |||
@@ -13,10 +14,9 @@ | |||
font-weight: normal; | |||
font-size: var(--text-sm); | |||
} | |||
min-height: var(--input-height); | |||
border-radius: $border-radius; | |||
font-weight: 400; | |||
padding: 8px 12px; | |||
padding: 6px 12px; | |||
cursor: default; | |||
color: var(--disabled-text-color); | |||
background-color: var(--disabled-control-bg); | |||
@@ -79,10 +79,9 @@ | |||
.grid-static-col, | |||
.row-index { | |||
height: 39px; | |||
padding: var(--padding-sm) var(--padding-md); | |||
height: 34px; | |||
padding: 8px; | |||
max-height: 200px; | |||
// border-right: 1px solid var(--border-color); | |||
} | |||
.grid-row-check { | |||
@@ -108,6 +107,7 @@ | |||
.grid-row > .row { | |||
.col:last-child { | |||
margin-right: calc(-1 * var(--margin-sm)); | |||
border-right: none; | |||
} | |||
.col { | |||
@@ -149,7 +149,7 @@ | |||
} | |||
textarea { | |||
height: 40px !important; | |||
height: 37px !important; | |||
} | |||
.form-control { | |||
@@ -157,7 +157,7 @@ | |||
border: 0px; | |||
padding-top: 8px; | |||
padding-bottom: 9px; | |||
height: 40px; | |||
height: 34px; | |||
} | |||
.link-btn { | |||
@@ -196,6 +196,10 @@ | |||
} | |||
} | |||
.grid-static-col[data-fieldtype="Check"] .static-area { | |||
padding-top: 2px; | |||
} | |||
.grid-static-col[data-fieldtype="Rating"] .field-area { | |||
margin-top: 1rem; | |||
margin-left: 1rem; | |||
@@ -1,6 +1,12 @@ | |||
@import "../common/form.scss"; | |||
@import '~cropperjs/dist/cropper.min'; | |||
.std-form-layout > .form-layout > .form-page { | |||
border-radius: var(--border-radius-md); | |||
box-shadow: var(--card-shadow); | |||
background-color: var(--card-bg); | |||
} | |||
.form-section, .form-dashboard-section { | |||
margin: 0px; | |||
@@ -12,6 +18,7 @@ | |||
.section-head { | |||
@extend .head-title; | |||
font-size: var(--text-base); | |||
width: 100%; | |||
padding: var(--padding-sm) var(--padding-md); | |||
margin: 0; | |||
@@ -47,8 +54,12 @@ | |||
.form-section.card-section, | |||
.form-dashboard-section { | |||
margin-bottom: var(--margin-lg); | |||
@extend .frappe-card; | |||
border-bottom: 1px solid var(--gray-200); | |||
padding: var(--padding-xs); | |||
} | |||
.row.form-section.card-section.visible-section:last-child { | |||
border-bottom: none; | |||
} | |||
.form-dashboard-section { | |||
@@ -57,9 +68,8 @@ | |||
} | |||
.section-body { | |||
display: block; | |||
padding-left: var(--padding-md); | |||
padding-right: var(--padding-md); | |||
padding-bottom: var(--padding-md); | |||
padding: var(--padding-md); | |||
padding-top: 0; | |||
} | |||
} | |||
@@ -85,7 +95,8 @@ | |||
.comment-box { | |||
@include card(); | |||
padding: 25px var(--padding-xl); | |||
margin-top: var(--margin-lg); | |||
padding: var(--padding-lg); | |||
.comment-input-header { | |||
@extend .head-title; | |||
margin-bottom: var(--margin-sm); | |||
@@ -304,19 +315,18 @@ | |||
} | |||
.form-tabs-list { | |||
margin-bottom: var(--margin-lg); | |||
padding-left: var(--padding-xs); | |||
border-bottom: 1px solid var(--gray-200); | |||
.form-tabs { | |||
.nav-item { | |||
.nav-link { | |||
padding-bottom: var(--padding-md); | |||
color: var(--gray-700); | |||
padding-left: 0; | |||
padding-right: 0; | |||
margin-right: var(--margin-xl); | |||
padding: var(--padding-md) 0; | |||
margin: 0 var(--margin-md); | |||
&.active { | |||
font-weight: 500; | |||
font-weight: 600; | |||
border-bottom: 1px solid var(--primary); | |||
color: var(--text-color); | |||
} | |||
@@ -507,10 +507,10 @@ def get_timespan_date_range(timespan): | |||
"yesterday": lambda: (add_to_date(today, days=-1),) * 2, | |||
"today": lambda: (today, today), | |||
"tomorrow": lambda: (add_to_date(today, days=1),) * 2, | |||
"this week": lambda: (get_first_day_of_week(today), today), | |||
"this month": lambda: (get_first_day(today), today), | |||
"this quarter": lambda: (get_quarter_start(today), today), | |||
"this year": lambda: (get_year_start(today), today), | |||
"this week": lambda: (get_first_day_of_week(today), get_last_day_of_week(today)), | |||
"this month": lambda: (get_first_day(today), get_last_day(today)), | |||
"this quarter": lambda: (get_quarter_start(today), get_quarter_ending(today)), | |||
"this year": lambda: (get_year_start(today), get_year_ending(today)), | |||
"next week": lambda: (get_first_day_of_week(add_to_date(today, days=7)), get_last_day_of_week(add_to_date(today, days=7))), | |||
"next month": lambda: (get_first_day(add_to_date(today, months=1)), get_last_day(add_to_date(today, months=1))), | |||
"next quarter": lambda: (get_quarter_start(add_to_date(today, months=3)), get_quarter_ending(add_to_date(today, months=3))), | |||
@@ -1,4 +1,4 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors | |||
# License: MIT. See LICENSE | |||
import frappe | |||
@@ -17,6 +17,20 @@ class MaxFileSizeReachedError(frappe.ValidationError): | |||
pass | |||
def safe_b64decode(binary: bytes) -> bytes: | |||
"""Adds padding if doesn't already exist before decoding. | |||
This attempts to avoid the `binascii.Error: Incorrect padding` error raised | |||
when the number of trailing = is simply not enough :crie:. Although, it may | |||
be an indication of corrupted data. | |||
Refs: | |||
* https://en.wikipedia.org/wiki/Base64 | |||
* https://stackoverflow.com/questions/2941995/python-ignore-incorrect-padding-error-when-base64-decoding | |||
""" | |||
return base64.b64decode(binary + b"===") | |||
def get_file_url(file_data_name): | |||
data = frappe.db.get_value("File", file_data_name, ["file_name", "file_url"], as_dict=True) | |||
return data.file_url or data.file_name | |||
@@ -112,7 +126,7 @@ def get_uploaded_content(): | |||
if 'filedata' in frappe.form_dict: | |||
if "," in frappe.form_dict.filedata: | |||
frappe.form_dict.filedata = frappe.form_dict.filedata.rsplit(",", 1)[1] | |||
frappe.uploaded_content = base64.b64decode(frappe.form_dict.filedata) | |||
frappe.uploaded_content = safe_b64decode(frappe.form_dict.filedata) | |||
frappe.uploaded_filename = frappe.form_dict.filename | |||
return frappe.uploaded_filename, frappe.uploaded_content | |||
else: | |||
@@ -126,7 +140,7 @@ def save_file(fname, content, dt, dn, folder=None, decode=False, is_private=0, d | |||
if b"," in content: | |||
content = content.split(b",")[1] | |||
content = base64.b64decode(content) | |||
content = safe_b64decode(content) | |||
file_size = check_max_file_size(content) | |||
content_hash = get_content_hash(content) | |||
@@ -69,15 +69,15 @@ frappe.ready(function() { | |||
const confirm_password = $('#confirm_password').val() | |||
if (!args.old_password && !args.key) { | |||
frappe.msgprint({ | |||
title: "{{ _('Message') }}", | |||
message: "{{ _('Old Password Required.') }}", | |||
title: "{{ _('Missing Value') }}", | |||
message: "{{ _('Please enter your old password.') }}", | |||
clear: true | |||
}); | |||
} | |||
if (!args.new_password) { | |||
frappe.msgprint({ | |||
title: "{{ _('Message') }}", | |||
message: "{{ _('New Password Required.') }}", | |||
title: "{{ _('Missing Value') }}", | |||
message: "{{ _('Please enter your new password.') }}", | |||
clear: true | |||
}); | |||
} | |||
@@ -110,8 +110,8 @@ frappe.ready(function() { | |||
.html("{{ _('Status Updated') }}"); | |||
if(r.message) { | |||
frappe.msgprint({ | |||
title: "{{ _('Message') }}", | |||
message: "{{ _('Password Updated') }}", | |||
title: "{{ _('Password set') }}", | |||
message: "{{ _('Your new password has been set successfully.') }}", | |||
// password is updated successfully | |||
// clear any server message | |||
clear: true | |||