diff --git a/frappe/__init__.py b/frappe/__init__.py index b3f1ed3bda..056832cd3f 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -17,7 +17,7 @@ from faker import Faker from .exceptions import * from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader) -__version__ = '10.1.63' +__version__ = '10.1.64' __title__ = "Frappe Framework" local = Local() diff --git a/frappe/client.py b/frappe/client.py index 3faa7e0129..60a2a9a00f 100644 --- a/frappe/client.py +++ b/frappe/client.py @@ -28,7 +28,7 @@ def get_list(doctype, fields=None, filters=None, order_by=None, :param limit_start: Start at this index :param limit_page_length: Number of records to be returned (default 20)''' if frappe.is_table(doctype): - check_parent_permission(parent) + check_parent_permission(parent, doctype) return frappe.get_list(doctype, fields=fields, filters=filters, order_by=order_by, limit_start=limit_start, limit_page_length=limit_page_length, ignore_permissions=False) @@ -45,7 +45,7 @@ def get(doctype, name=None, filters=None, parent=None): :param name: return document of this `name` :param filters: If name is not set, filter by these values and return the first match''' if frappe.is_table(doctype): - check_parent_permission(parent) + check_parent_permission(parent, doctype) if filters and not name: name = frappe.db.get_value(doctype, json.loads(filters)) @@ -66,7 +66,7 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren :param fieldname: Field to be returned (default `name`) :param filters: dict or string for identifying the record''' if frappe.is_table(doctype): - check_parent_permission(parent) + check_parent_permission(parent, doctype) if not frappe.has_permission(doctype): frappe.throw(_("No permission for {0}".format(doctype)), frappe.PermissionError) @@ -360,8 +360,13 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder return f.as_dict() -def check_parent_permission(parent): +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}): + 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! diff --git a/frappe/config/setup.py b/frappe/config/setup.py index 42c29fe866..7ed9d837ce 100644 --- a/frappe/config/setup.py +++ b/frappe/config/setup.py @@ -177,6 +177,11 @@ def get_data(): "name": "Auto Email Report", "description": _("Setup Reports to be emailed at regular intervals"), }, + { + "type": "doctype", + "name": "Newsletter", + "description": _("Create and manage newsletter") + } ] }, { diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 2585af3479..d0a7433e72 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -94,8 +94,8 @@ class Communication(Document): if self.communication_type in ("Communication", "Comment"): # send new comment to listening clients frappe.publish_realtime('new_communication', self.as_dict(), - doctype=self.reference_doctype, docname=self.reference_name, - after_commit=True) + doctype=self.reference_doctype, docname=self.reference_name, + after_commit=True) if self.communication_type == "Comment": notify_mentions(self) @@ -108,7 +108,7 @@ class Communication(Document): else: # reference_name contains the user who is addressed in the messages' page comment frappe.publish_realtime('new_message', self.as_dict(), - user=self.reference_name, after_commit=True) + user=self.reference_name, after_commit=True) def on_update(self): """Update parent status as `Open` or `Replied`.""" diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js index 36c2488aab..6177aaf86f 100644 --- a/frappe/core/doctype/data_import/data_import.js +++ b/frappe/core/doctype/data_import/data_import.js @@ -200,6 +200,13 @@ frappe.data_import.download_dialog = function(frm) { "options": "Excel\nCSV", "default": "Excel" }, + { + "label": __("Download with Data"), + "fieldname": "with_data", + "fieldtype": "Check", + "hidden": !frm.doc.overwrite, + "default": 1 + }, { "label": __("Select All"), "fieldname": "select_all", @@ -270,7 +277,7 @@ frappe.data_import.download_dialog = function(frm) { doctype: frm.doc.reference_doctype, parent_doctype: frm.doc.reference_doctype, select_columns: JSON.stringify(columns), - with_data: frm.doc.overwrite, + with_data: frm.doc.overwrite && data.with_data, all_doctypes: true, file_type: data.file_type, template: true diff --git a/frappe/hooks.py b/frappe/hooks.py index 70558b2ff8..fdfe405c95 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -12,7 +12,7 @@ source_link = "https://github.com/frappe/frappe" app_license = "MIT" develop_version = '12.x.x-develop' -staging_version = '11.0.3-beta.29' +staging_version = '11.0.3-beta.30' app_email = "info@frappe.io" diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 449bfa0262..109ac8e4a0 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -476,7 +476,8 @@ class BaseDocument(object): setattr(self, df.fieldname, values.name) for _df in fields_to_fetch: - setattr(self, _df.fieldname, values[_df.fetch_from.split('.')[-1]]) + if self.docstatus != 1 or _df.allow_on_submit: + setattr(self, _df.fieldname, values[_df.fetch_from.split('.')[-1]]) notify_link_count(doctype, docname) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index f09cc042e1..281ff589a0 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -14,6 +14,7 @@ import frappe.permissions from datetime import datetime import frappe, json, copy, re from frappe.model import optional_fields +from frappe.client import check_parent_permission from frappe.model.utils.user_settings import get_user_settings, update_user_settings from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr @@ -680,6 +681,19 @@ def get_list(doctype, *args, **kwargs): '''wrapper for DatabaseQuery''' kwargs.pop('cmd', None) kwargs.pop('ignore_permissions', None) + + # If doctype is child table + if frappe.is_table(doctype): + # Example frappe.db.get_list('Purchase Receipt Item', {'parent': 'Purchase Receipt'}) + # Here purchase receipt is the parent doctype of the child doctype Purchase Receipt Item + + if not kwargs.get('parent'): + frappe.flags.error_message = _('Parent is required to get child table data') + raise frappe.PermissionError(doctype) + + check_parent_permission(kwargs.get('parent'), doctype) + del kwargs['parent'] + return DatabaseQuery(doctype).execute(None, *args, **kwargs) def is_parent_only_filter(doctype, filters): diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 94282354b5..7b22ccadbf 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -82,6 +82,7 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F frappe.delete_doc(doctype, old) frappe.clear_cache() + frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', doctype=doctype) return new diff --git a/frappe/public/js/frappe/ui/toolbar/search.js b/frappe/public/js/frappe/ui/toolbar/search.js index f5c7714179..0554d0af09 100644 --- a/frappe/public/js/frappe/ui/toolbar/search.js +++ b/frappe/public/js/frappe/ui/toolbar/search.js @@ -361,7 +361,7 @@ frappe.search.SearchDialog = Class.extend({ no_results_status: (keyword) => __("

No results found for '" + keyword + "' in Global Search

"), get_results: function(keywords, callback) { - var start = 0, limit = 100; + var start = 0, limit = 1000; var results = frappe.search.utils.get_nav_results(keywords); frappe.search.utils.get_global_results(keywords, start, limit) .then(function(global_results) { diff --git a/frappe/templates/styles/standard.css b/frappe/templates/styles/standard.css index fa60c8d45f..e8c14210f9 100644 --- a/frappe/templates/styles/standard.css +++ b/frappe/templates/styles/standard.css @@ -150,7 +150,9 @@ table td div { /* hack for webkit specific browser */ @media (-webkit-min-device-pixel-ratio:0) { - thead, tfoot { display: table-row-group; } + thead, tfoot { + display: table-header-group; + } } [document-status] { diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py index c103c37212..1ac3667138 100644 --- a/frappe/utils/redis_wrapper.py +++ b/frappe/utils/redis_wrapper.py @@ -121,17 +121,17 @@ class RedisWrapper(redis.Redis): except redis.exceptions.ConnectionError: pass - def lpush(self, key, *values): - super(redis.Redis, self).lpush(self.make_key(key), *values) + def lpush(self, key, value): + super(RedisWrapper, self).lpush(self.make_key(key), value) - def rpush(self, key, *values): - super(redis.Redis, self).rpush(self.make_key(key), *values) + def rpush(self, key, value): + super(RedisWrapper, self).rpush(self.make_key(key), value) def lpop(self, key): - return super(redis.Redis, self).lpop(self.make_key(key)) + return super(RedisWrapper, self).lpop(self.make_key(key)) def llen(self, key): - return super(redis.Redis, self).llen(self.make_key(key)) + return super(RedisWrapper, self).llen(self.make_key(key)) def hset(self, name, key, value, shared=False): _name = self.make_key(name, shared=shared) @@ -143,14 +143,14 @@ class RedisWrapper(redis.Redis): # set in redis try: - super(redis.Redis, self).hset(_name, + super(RedisWrapper, self).hset(_name, key, pickle.dumps(value)) except redis.exceptions.ConnectionError: pass def hgetall(self, name): return {key: pickle.loads(value) for key, value in - iteritems(super(redis.Redis, self).hgetall(self.make_key(name)))} + iteritems(super(RedisWrapper, self).hgetall(self.make_key(name)))} def hget(self, name, key, generator=None, shared=False): _name = self.make_key(name, shared=shared) @@ -162,7 +162,7 @@ class RedisWrapper(redis.Redis): value = None try: - value = super(redis.Redis, self).hget(_name, key) + value = super(RedisWrapper, self).hget(_name, key) except redis.exceptions.ConnectionError: pass @@ -184,7 +184,7 @@ class RedisWrapper(redis.Redis): if key in frappe.local.cache[_name]: del frappe.local.cache[_name][key] try: - super(redis.Redis, self).hdel(_name, key) + super(RedisWrapper, self).hdel(_name, key) except redis.exceptions.ConnectionError: pass @@ -196,31 +196,31 @@ class RedisWrapper(redis.Redis): def hkeys(self, name): try: - return super(redis.Redis, self).hkeys(self.make_key(name)) + return super(RedisWrapper, self).hkeys(self.make_key(name)) except redis.exceptions.ConnectionError: return [] def sadd(self, name, *values): """Add a member/members to a given set""" - super(redis.Redis, self).sadd(self.make_key(name), *values) + super(RedisWrapper, self).sadd(self.make_key(name), *values) def srem(self, name, *values): """Remove a specific member/list of members from the set""" - super(redis.Redis, self).srem(self.make_key(name), *values) + super(RedisWrapper, self).srem(self.make_key(name), *values) def sismember(self, name, value): """Returns True or False based on if a given value is present in the set""" - return super(redis.Redis, self).sismember(self.make_key(name), value) + return super(RedisWrapper, self).sismember(self.make_key(name), value) def spop(self, name): """Removes and returns a random member from the set""" - return super(redis.Redis, self).spop(self.make_key(name)) + return super(RedisWrapper, self).spop(self.make_key(name)) def srandmember(self, name, count=None): """Returns a random member from the set""" - return super(redis.Redis, self).srandmember(self.make_key(name)) + return super(RedisWrapper, self).srandmember(self.make_key(name)) def smembers(self, name): """Return all members of the set""" - return super(redis.Redis, self).smembers(self.make_key(name)) + return super(RedisWrapper, self).smembers(self.make_key(name)) diff --git a/frappe/utils/response.py b/frappe/utils/response.py index df50514c94..9eec6045fa 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -38,6 +38,7 @@ def build_response(response_type=None): response_type_map = { 'csv': as_csv, + 'txt': as_txt, 'download': as_raw, 'json': as_json, 'page': as_page, @@ -55,6 +56,14 @@ def as_csv(): response.data = frappe.response['result'] return response +def as_txt(): + response = Response() + response.mimetype = 'text' + response.charset = 'utf-8' + response.headers["Content-Disposition"] = ("attachment; filename=\"%s.txt\"" % frappe.response['doctype'].replace(' ', '_')).encode("utf-8") + response.data = frappe.response['result'] + return response + def as_raw(): response = Response() response.mimetype = frappe.response.get("content_type") or mimetypes.guess_type(frappe.response['filename'])[0] or "application/unknown"