Merge branch 'staging' into develop

This commit is contained in:
Saurabh 2018-12-07 17:56:57 +05:30
commit 7b7a492380
11 changed files with 163 additions and 78 deletions

View file

@ -15,7 +15,7 @@ frappe.ui.form.on('Prepared Report', {
<tbody></tbody>
</table>`);
const filters = JSON.parse(JSON.parse(frm.doc.filters));
const filters = JSON.parse(frm.doc.filters);
Object.keys(filters).forEach(key => {
const filter_row = $(`<tr>

View file

@ -6,6 +6,7 @@
from __future__ import unicode_literals
import base64
import json
import io
import frappe
from frappe.model.document import Document
@ -15,7 +16,9 @@ from frappe.core.doctype.file.file import remove_all
from frappe.utils.csvutils import to_csv, read_csv_content_from_attached_file
from frappe.desk.form.load import get_attachments
from frappe.core.doctype.file.file import download_file
from frappe.utils import gzip_compress, gzip_decompress
from six import PY2
from frappe.utils import encode
class PreparedReport(Document):
@ -35,8 +38,8 @@ class PreparedReport(Document):
def run_background(instance):
report = frappe.get_doc("Report", instance.ref_report_doctype)
result = generate_report_result(report, filters=json.loads(instance.filters), user=instance.owner)
create_csv_file(result['columns'], result['result'], 'Prepared Report', instance.name)
result = generate_report_result(report, filters=instance.filters, user=instance.owner)
create_json_gz_file(result['result'], 'Prepared Report', instance.name)
instance.status = "Completed"
instance.columns = json.dumps(result["columns"])
@ -45,65 +48,52 @@ def run_background(instance):
frappe.publish_realtime(
'report_generated',
{"report_name": instance.report_name},
{"report_name": instance.report_name, "name": instance.name},
user=frappe.session.user
)
def remove_header_meta(columns):
column_list = []
columns_header = get_columns_dict(columns)
for idx in range(len(columns)):
column_list.append(columns_header[idx]['label'])
return column_list
def create_json_gz_file(data, dt, dn):
# Storing data in CSV file causes information loss
# Reports like P&L Statement were completely unsuable because of this
json_filename = '{0}.json.gz'.format(frappe.utils.data.format_datetime(frappe.utils.now(), "Y-m-d-H:M"))
encoded_content = frappe.safe_encode(frappe.as_json(data))
compressed_content = gzip_compress(encoded_content)
def create_csv_file(columns, data, dt, dn):
csv_filename = '{0}.csv'.format(frappe.utils.data.format_datetime(frappe.utils.now(), "Y-m-d-H:M"))
rows = []
if data:
columns_without_meta = remove_header_meta(columns)
row = data[0]
if type(row) == list:
rows = [tuple(columns_without_meta)] + data
else:
for row in data:
new_row = []
for col in columns:
key = col.get('fieldname') or col.get('label')
new_row.append(row.get(key, ''))
rows.append(new_row)
rows = [tuple(columns_without_meta)] + rows
encoded = base64.b64encode(frappe.safe_encode(to_csv(rows)))
# Call save() file function to upload and attach the file
_file = frappe.get_doc({
"doctype": "File",
"file_name": csv_filename,
"file_name": json_filename,
"attached_to_doctype": dt,
"attached_to_name": dn,
"content": encoded,
"content": compressed_content,
"decode": True})
_file.save()
@frappe.whitelist()
def get_report_attachment_data(dn):
doc = frappe.get_doc("Prepared Report", dn)
data = read_csv_content_from_attached_file(doc)
return {
'columns': data[0],
'result': data[1:]
}
@frappe.whitelist()
def download_attachment(dn):
attachment = get_attachments("Prepared Report", dn)[0]
download_file(attachment.file_url)
frappe.local.response.filename = attachment.file_name[:-2]
frappe.local.response.filecontent = gzip_decompress(get_file(attachment.name)[1])
frappe.local.response.type = "binary"
def get_file(fname):
"""Returns [`file_name`, `content`] for given file name `fname`"""
_file = frappe.get_doc("File", {"file_name": fname})
file_path = _file.get_full_path()
# read the file
if PY2:
with open(encode(file_path)) as f:
content = f.read()
else:
with io.open(encode(file_path), mode='rb') as f:
content = f.read()
try:
# for plain text files
content = content.decode()
except UnicodeDecodeError:
# for .png, .jpg, etc
pass
return [file_path.rsplit("/", 1)[-1], content]

View file

@ -12,10 +12,11 @@ from frappe.utils import flt, cint, get_html_format, cstr, get_url_to_form
from frappe.model.utils import render_include
from frappe.translate import send_translations
import frappe.desk.reportview
from frappe.utils.csvutils import read_csv_content_from_attached_file
from frappe.permissions import get_role_permissions
from six import string_types, iteritems
from datetime import timedelta
from frappe.utils.file_manager import get_file
from frappe.utils import gzip_decompress
def get_report_doc(report_name):
doc = frappe.get_doc("Report", report_name)
@ -99,7 +100,9 @@ def background_enqueue_run(report_name, filters=None, user=None):
frappe.get_doc({
"doctype": "Prepared Report",
"report_name": report_name,
"filters": json.dumps(filters),
# This looks like an insanity but, without this it'd be very hard to find Prepared Reports matching given condition
# We're ensuring that spacing is consistent. e.g. JS seems to put no spaces after ":", Python on the other hand does.
"filters": json.dumps(json.loads(filters)),
"ref_report_doctype": report_name,
"report_type": report.report_type,
"query": report.query,
@ -108,6 +111,7 @@ def background_enqueue_run(report_name, filters=None, user=None):
track_instance.insert(ignore_permissions=True)
frappe.db.commit()
return {
"name": track_instance.name,
"redirect_url": get_url_to_form("Prepared Report", track_instance.name)
}
@ -164,9 +168,10 @@ def run(report_name, filters=None, user=None):
filters = json.loads(filters)
dn = filters.get("prepared_report_name")
filters.pop("prepared_report_name", None)
else:
dn = ""
result = get_prepared_report_result(report, filters, dn)
result = get_prepared_report_result(report, filters, dn, user)
else:
result = generate_report_result(report, filters, user)
@ -175,9 +180,10 @@ def run(report_name, filters=None, user=None):
return result
def get_prepared_report_result(report, filters, dn=""):
def get_prepared_report_result(report, filters, dn="", user=None):
latest_report_data = {}
doc_list = frappe.get_all("Prepared Report", filters={"status": "Completed", "report_name": report.name})
# Only look for completed prepared reports with given filters.
doc_list = frappe.get_all("Prepared Report", filters={"status": "Completed", "report_name": report.name, "filters": json.dumps(filters), "owner": user})
doc = None
if len(doc_list):
if dn:
@ -187,11 +193,15 @@ def get_prepared_report_result(report, filters, dn=""):
# Get latest
doc = frappe.get_doc("Prepared Report", doc_list[0])
data = read_csv_content_from_attached_file(doc)
# Prepared Report data is stored in a GZip compressed JSON file
attached_file_name = frappe.db.get_value("File", {"attached_to_doctype": doc.doctype, "attached_to_name":doc.name}, "name")
compressed_content = get_file(attached_file_name)[1]
uncompressed_content = gzip_decompress(compressed_content)
data = json.loads(uncompressed_content)
if data:
latest_report_data = {
"columns": json.loads(doc.columns) if doc.columns else data[0],
"result": data[1:]
"result": data
}
latest_report_data.update({

View file

@ -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.33'
staging_version = '11.0.3-beta.34'
app_email = "info@frappe.io"

View file

@ -232,3 +232,4 @@ frappe.patches.v11_0.multiple_references_in_events
frappe.patches.v11_0.set_allow_self_approval_in_workflow
execute:frappe.db.sql('ALTER table `tabSeries` ADD PRIMARY KEY IF NOT EXISTS (name)')
frappe.patches.v11_0.migrate_report_settings_for_new_listview
frappe.patches.v11_0.delete_all_prepared_reports

View file

@ -0,0 +1,6 @@
import frappe
def execute():
prepared_reports = frappe.get_all("Prepared Report")
for report in prepared_reports:
frappe.delete_doc("Prepared Report", report.name)

View file

@ -5,6 +5,10 @@ const Block = Quill.import('blots/block');
Block.tagName = 'DIV';
Quill.register(Block, true);
const CodeBlockContainer = Quill.import('formats/code-block-container');
CodeBlockContainer.tagName = 'PRE';
Quill.register(CodeBlockContainer, true);
// table
const Table = Quill.import('formats/table-container');
const superCreate = Table.create.bind(Table);

View file

@ -63,9 +63,17 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
setup_events() {
frappe.realtime.on("report_generated", (data) => {
if(data.report_name) {
let alert_message = `Report ${this.report_name} generated.
<a target='_blank' href="#query-report/${this.report_name}">View</a>`;
frappe.show_alert({message: alert_message, indicator: 'orange'});
this.prepared_report_action = "Rebuild";
// If generated report and currently active Prepared Report has same fiters
// then refresh the Prepared Report
// Otherwise show alert with the link to the Prepared Report
if(data.name == this.prepared_report_doc_name) {
this.refresh();
} else {
let alert_message = `Report ${this.report_name} generated.
<a target='_blank' href="#query-report/${this.report_name}/?prepared_report_name=${data.name}">View</a>`;
frappe.show_alert({message: alert_message, indicator: 'orange'});
}
}
});
}
@ -93,6 +101,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.page_title = __(this.report_name);
this.menu_items = this.get_menu_items();
this.datatable = null;
this.prepared_report_action = "New";
frappe.run_serially([
() => this.get_report_doc(),
@ -266,8 +275,22 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.hide_status();
if (data.prepared_report){
if (data.prepared_report) {
this.prepared_report = true;
const query_string = frappe.utils.get_query_string(frappe.get_route_str());
const query_params = frappe.utils.get_query_params(query_string);
// If query_string contains prepared_report_name then set filters
// to match the mentioned prepared report doc and disable editing
if(query_params.prepared_report_name) {
this.prepared_report_action = "Edit";
const filters_from_report = JSON.parse(data.doc.filters);
Object.values(this.filters).forEach(function(field) {
if (filters_from_report[field.fieldname]) {
field.set_input(filters_from_report[field.fieldname]);
}
field.input.disabled = true;
});
}
this.add_prepared_report_buttons(data.doc);
}
this.toggle_message(false);
@ -297,19 +320,43 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
+"dn="+encodeURIComponent(doc.name)));
});
this.show_status(__(`
<span class="indicator orange">This report was <a href=#Form/Prepared%20Report/${doc.name}>generated</a>
on ${frappe.datetime.convert_to_user_tz(doc.report_end_time)}.
<a href=#List/Prepared%20Report>See all past reports</a>.</span>
`));
const part1 = __('This report was generated {0}.', [frappe.datetime.comment_when(doc.report_end_time)]);
const part2 = __('To get the updated report, click on {0}.', [__('Rebuild')]);
const part3 = __('See all past reports.');
this.show_status(`
<span class="indicator orange">
${part1}
${part2}
<a href="#List/Prepared%20Report?report_name=${this.report_name}">${part3}</a>
</span>
`);
};
// if
this.page.set_primary_action(
__("Generate New Report"),
this.generate_background_report.bind(this)
);
// Three cases
// 1. First time with given filters, no data.
// 2. Showing data from specific report
// 3. Showing data from an old report without specific report name
if(this.prepared_report_action == "New") {
this.page.set_primary_action(
__("Generate New Report"),
() => {
this.generate_background_report();
}
);
} else if(this.prepared_report_action == "Edit") {
this.page.set_primary_action(
__("Edit"),
() => {
frappe.set_route(frappe.get_route());
}
);
} else if(this.prepared_report_action == "Rebuild"){
this.page.set_primary_action(
__("Rebuild"),
this.generate_background_report.bind(this)
);
}
}
generate_background_report() {
@ -327,6 +374,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
callback: resolve
})).then(r => {
const data = r.message;
// Rememeber the name of Prepared Report doc
this.prepared_report_doc_name = data.name;
let alert_message = `Report initiated. You can track its status
<a class='text-info' target='_blank' href=${data.redirect_url}>here</a>`;
frappe.show_alert({message: alert_message, indicator: 'orange'});
@ -898,6 +947,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
reset_report_view() {
this.hide_status();
this.toggle_nothing_to_show(true);
this.refresh();
}
toggle_loading(flag) {
@ -912,6 +962,10 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
Please set the appropriate filters and then generate a new one.`);
}
this.toggle_message(flag, message);
if(flag){
this.prepared_report_action = "New";
}
this.add_prepared_report_buttons();
}
toggle_message(flag, message) {

View file

@ -14,6 +14,8 @@ from email.utils import parseaddr, formataddr
from frappe.utils.data import *
from six.moves.urllib.parse import quote
from six import text_type, string_types
import io
from gzip import GzipFile
default_fields = ['doctype', 'name', 'owner', 'creation', 'modified', 'modified_by',
'parent', 'parentfield', 'parenttype', 'idx', 'docstatus']
@ -620,3 +622,21 @@ def call(fn, *args, **kwargs):
bench --site erpnext.local execute frappe.utils.call --args '''["frappe.get_all", "Activity Log"]''' --kwargs '''{"fields": ["user", "creation", "full_name"], "filters":{"Operation": "Login", "Status": "Success"}, "limit": "10"}'''
"""
return json.loads(frappe.as_json(frappe.call(fn, *args, **kwargs)))
# Following methods are aken as-is from Python 3 codebase
# since gzip.compress and gzip.decompress are not available in Python 2.7
def gzip_compress(data, compresslevel=9):
"""Compress data in one shot and return the compressed string.
Optional argument is the compression level, in range of 0-9.
"""
buf = io.BytesIO()
with GzipFile(fileobj=buf, mode='wb', compresslevel=compresslevel) as f:
f.write(data)
return buf.getvalue()
def gzip_decompress(data):
"""Decompress a gzip compressed string in one shot.
Return the decompressed string.
"""
with GzipFile(fileobj=io.BytesIO(data)) as f:
return f.read()

View file

@ -22,7 +22,7 @@
"awesomplete": "^1.1.2",
"cookie": "^0.3.1",
"express": "^4.16.2",
"frappe-datatable": "^1.6.0",
"frappe-datatable": "^1.6.1",
"frappe-gantt": "^0.1.0",
"fuse.js": "^3.2.0",
"highlight.js": "^9.12.0",

View file

@ -1517,10 +1517,10 @@ forwarded@~0.1.2:
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
frappe-datatable@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.6.0.tgz#c86c2c7fc054a500c70cdf6c6362b5fed63c6fe6"
integrity sha512-40iwguZr0w+X0MV/yTzsYDoKlH7b/GuOq6o2lEOrbVT3p4dZYpalxQmO8mkM9gKjNjSgGMVL1r6Se8peq80Qqg==
frappe-datatable@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.6.1.tgz#e57850923b5f307fd02328c522c5abe35a17dd89"
integrity sha512-u2l4I2Pwu4jTLSBF7vW7EDwzcRdfrlKbgaUCyhDUYlOhWl0sRt6rO2ZmIhU7znSYz7TNkKkAt7hkI+x+Mg6ONw==
dependencies:
hyperlist "^1.0.0-beta"
lodash "^4.17.5"