Merge branch 'develop' of https://github.com/frappe/frappe into develop

This commit is contained in:
Sachin Mane 2019-05-20 18:51:39 +05:30
commit b1c63dbe95
278 changed files with 8992 additions and 14584 deletions

View file

@ -140,6 +140,7 @@
"expect": true,
"context": true,
"before": true,
"beforeEach": true
"beforeEach": true,
"qz": true
}
}

View file

@ -1,29 +0,0 @@
include MANIFEST.in
include requirements.txt
include *.json
include *.md
include *.py
recursive-include frappe *.css
recursive-include frappe *.dat
recursive-include frappe *.eot
recursive-include frappe *.gif
recursive-include frappe *.html
recursive-include frappe *.jpg
recursive-include frappe *.js
recursive-include frappe *.json
recursive-include frappe *.md
recursive-include frappe *.otf
recursive-include frappe *.png
recursive-include frappe *.py
recursive-include frappe *.sql
recursive-include frappe *.svg
recursive-include frappe *.swf
recursive-include frappe *.ttf
recursive-include frappe *.woff
recursive-include frappe *.xml
recursive-include frappe *.csv
recursive-include frappe *.ico
recursive-include frappe *.less
recursive-include frappe *.txt
recursive-include frappe/public *
recursive-exclude * *.pyc

View file

@ -0,0 +1,61 @@
context('FileUploader', () => {
before(() => {
cy.login('Administrator', 'qwe');
cy.visit('/desk');
});
function open_upload_dialog() {
cy.window().its('frappe').then(frappe => {
new frappe.ui.FileUploader();
});
}
it('upload dialog api works', () => {
open_upload_dialog();
cy.get_open_dialog().should('contain', 'Drag and drop files');
cy.hide_dialog();
});
it('should accept dropped files', () => {
open_upload_dialog();
cy.fixture('example.json').then(fileContent => {
cy.get_open_dialog().find('.file-upload-area').upload(
{ fileContent, fileName: 'example.json', mimeType: 'application/json' },
{ subjectType: 'drag-n-drop' },
);
cy.get_open_dialog().find('.file-info').should('contain', 'example.json');
cy.server();
cy.route('POST', '/api/method/upload_file').as('upload_file');
cy.get_open_dialog().find('.btn-primary').click();
cy.wait('@upload_file').its('status').should('be', 200);
cy.get('.modal:visible').should('not.exist');
});
});
it('should accept uploaded files', () => {
open_upload_dialog();
cy.get_open_dialog().find('a:contains("uploaded file")').click();
cy.get_open_dialog().find('.tree-label:contains("example.json")').first().click();
cy.server();
cy.route('POST', '/api/method/upload_file').as('upload_file');
cy.get_open_dialog().find('.btn-primary').click();
cy.wait('@upload_file').its('response.body.message')
.should('have.property', 'file_url', '/private/files/example.json');
cy.get('.modal:visible').should('not.exist');
});
it('should accept web links', () => {
open_upload_dialog();
cy.get_open_dialog().find('a:contains("web link")').click();
cy.get_open_dialog().find('.file-web-link input').type('https://github.com');
cy.server();
cy.route('POST', '/api/method/upload_file').as('upload_file');
cy.get_open_dialog().find('.btn-primary').click();
cy.wait('@upload_file').its('response.body.message')
.should('have.property', 'file_url', 'https://github.com');
cy.get('.modal:visible').should('not.exist');
});
});

View file

@ -9,7 +9,7 @@ context('Table MultiSelect', () => {
cy.new_form('Assignment Rule');
cy.fill_field('__newname', name);
cy.fill_field('document_type', 'ToDo');
cy.fill_field('assign_condition', 'status=="Open"');
cy.fill_field('assign_condition', 'status=="Open"', 'Code');
cy.get('input[data-fieldname="users"]').focus().as('input');
cy.get('input[data-fieldname="users"] + ul').should('be.visible');
cy.get('@input').type('test{enter}', { delay: 100 });

View file

@ -1,3 +1,4 @@
import 'cypress-file-upload';
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
@ -38,7 +39,10 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype='Data') => {
let selector = `.form-control[data-fieldname="${fieldname}"]`;
if (fieldtype === 'Text Editor') {
selector = `[data-fieldname="${fieldname}"] .ql-editor`;
selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`;
}
if (fieldtype === 'Code') {
selector = `[data-fieldname="${fieldname}"] .ace_text-input`;
}
cy.get(selector).as('input');
@ -46,7 +50,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype='Data') => {
if (fieldtype === 'Select') {
return cy.get('@input').select(value);
} else {
return cy.get('@input').type(value);
return cy.get('@input').type(value, {waitForAnimations: false});
}
});
@ -75,3 +79,12 @@ Cypress.Commands.add('dialog', (title, fields) => {
return d;
});
});
Cypress.Commands.add('get_open_dialog', () => {
return cy.get('.modal:visible').last();
});
Cypress.Commands.add('hide_dialog', () => {
cy.get_open_dialog().find('.btn-modal-close').click();
cy.get('.modal:visible').should('not.exist');
});

View file

@ -16,7 +16,6 @@ from faker import Faker
# public
from .exceptions import *
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
from .utils.error import get_frame_locals
# Hamless for Python 3
# For Python 2 set default encoding to utf-8
@ -188,15 +187,20 @@ def connect(site=None, db_name=None):
local.db = get_db(user=db_name or local.conf.db_name)
set_user("Administrator")
def connect_read_only():
def connect_replica():
from frappe.database import get_db
user = local.conf.db_name
password = local.conf.db_password
local.read_only_db = get_db(local.conf.slave_host, local.conf.slave_db_name,
local.conf.slave_db_password)
if local.conf.different_credentials_for_replica:
user = local.conf.replica_db_name
password = local.conf.replica_db_password
local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password)
# swap db connections
local.master_db = local.db
local.db = local.read_only_db
local.primary_db = local.db
local.db = local.replica_db
def get_site_config(sites_path=None, site_path=None):
"""Returns `site_config.json` combined with `sites/common_site_config.json`.
@ -274,7 +278,7 @@ def errprint(msg):
if not request or (not "cmd" in local.form_dict) or conf.developer_mode:
print(msg)
error_log.append({"exc": msg, "locals": get_frame_locals()})
error_log.append({"exc": msg})
def log(msg):
"""Add to `debug_log`.
@ -496,16 +500,17 @@ def whitelist(allow_guest=False, xss_safe=False):
def read_only():
def innfn(fn):
def wrapper_fn(*args, **kwargs):
if conf.use_slave_for_read_only:
connect_read_only()
if conf.read_from_replica:
connect_replica()
try:
retval = fn(*args, **get_newargs(fn, kwargs))
except:
raise
finally:
if local and hasattr(local, 'master_db'):
if local and hasattr(local, 'primary_db'):
local.db.close()
local.db = local.master_db
local.db = local.primary_db
return retval
return wrapper_fn
@ -1262,7 +1267,7 @@ def get_all(doctype, *args, **kwargs):
:param fields: List of fields or `*`. Default is: `["name"]`.
:param filters: List of filters (see example).
:param order_by: Order By e.g. `modified desc`.
:param limit_page_start: Start results at record #. Default 0.
:param limit_start: Start results at record #. Default 0.
:param limit_page_length: No of records in the page. Default 20.
Example usage:
@ -1297,7 +1302,7 @@ def get_value(*args, **kwargs):
def as_json(obj, indent=1):
from frappe.utils.response import json_handler
return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler)
return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler, separators=(',', ': '))
def are_emails_muted():
from frappe.utils import cint
@ -1329,14 +1334,15 @@ def format(*args, **kwargs):
import frappe.utils.formatters
return frappe.utils.formatters.format_value(*args, **kwargs)
def get_print(doctype=None, name=None, print_format=None, style=None, html=None, as_pdf=False, doc=None, output = None, no_letterhead = 0):
def get_print(doctype=None, name=None, print_format=None, style=None, html=None, as_pdf=False, doc=None, output = None, no_letterhead = 0, password=None):
"""Get Print Format for given document.
:param doctype: DocType of document.
:param name: Name of document.
:param print_format: Print Format name. Default 'Standard',
:param style: Print Format style.
:param as_pdf: Return as PDF. Default False."""
:param as_pdf: Return as PDF. Default False.
:param password: Password to encrypt the pdf with. Default None"""
from frappe.website.render import build_page
from frappe.utils.pdf import get_pdf
@ -1347,15 +1353,19 @@ def get_print(doctype=None, name=None, print_format=None, style=None, html=None,
local.form_dict.doc = doc
local.form_dict.no_letterhead = no_letterhead
options = None
if password:
options = {'password': password}
if not html:
html = build_page("printview")
if as_pdf:
return get_pdf(html, output = output)
return get_pdf(html, output = output, options = options)
else:
return html
def attach_print(doctype, name, file_name=None, print_format=None, style=None, html=None, doc=None, lang=None, print_letterhead=True):
def attach_print(doctype, name, file_name=None, print_format=None, style=None, html=None, doc=None, lang=None, print_letterhead=True, password=None):
from frappe.utils import scrub_urls
if not file_name: file_name = name
@ -1374,12 +1384,12 @@ def attach_print(doctype, name, file_name=None, print_format=None, style=None, h
if int(print_settings.send_print_as_pdf or 0):
out = {
"fname": file_name + ".pdf",
"fcontent": get_print(doctype, name, print_format=print_format, style=style, html=html, as_pdf=True, doc=doc, no_letterhead=no_letterhead)
"fcontent": get_print(doctype, name, print_format=print_format, style=style, html=html, as_pdf=True, doc=doc, no_letterhead=no_letterhead, password=password)
}
else:
out = {
"fname": file_name + ".html",
"fcontent": scrub_urls(get_print(doctype, name, print_format=print_format, style=style, html=html, doc=doc, no_letterhead=no_letterhead)).encode("utf-8")
"fcontent": scrub_urls(get_print(doctype, name, print_format=print_format, style=style, html=html, doc=doc, no_letterhead=no_letterhead, password=password)).encode("utf-8")
}
local.flags.ignore_print_permissions = False

View file

@ -22,6 +22,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "document_type",
"fieldtype": "Link",
"hidden": 0,
@ -56,6 +57,7 @@
"collapsible": 0,
"columns": 0,
"description": "Higher priority rule will be applied first",
"fetch_if_empty": 0,
"fieldname": "priority",
"fieldtype": "Int",
"hidden": 0,
@ -88,6 +90,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "disabled",
"fieldtype": "Check",
"hidden": 0,
@ -120,6 +123,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_4",
"fieldtype": "Column Break",
"hidden": 0,
@ -153,6 +157,7 @@
"columns": 0,
"default": "Automatic Assignment",
"description": "Example: {{ subject }}",
"fetch_if_empty": 0,
"fieldname": "description",
"fieldtype": "Small Text",
"hidden": 0,
@ -185,6 +190,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "assignment_rules_section",
"fieldtype": "Section Break",
"hidden": 0,
@ -218,8 +224,9 @@
"collapsible": 0,
"columns": 0,
"description": "Simple Python Expression, Example: status == 'Open' and type == 'Bug'",
"fetch_if_empty": 0,
"fieldname": "assign_condition",
"fieldtype": "Small Text",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@ -250,6 +257,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_6",
"fieldtype": "Column Break",
"hidden": 0,
@ -282,8 +290,9 @@
"collapsible": 0,
"columns": 0,
"description": "Simple Python Expression, Example: Status in (\"Closed\", \"Cancelled\")",
"fetch_if_empty": 0,
"fieldname": "unassign_condition",
"fieldtype": "Small Text",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@ -314,6 +323,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "assign_to_users_section",
"fieldtype": "Section Break",
"hidden": 0,
@ -346,6 +356,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "rule",
"fieldtype": "Select",
"hidden": 0,
@ -379,6 +390,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "users",
"fieldtype": "Table MultiSelect",
"hidden": 0,
@ -412,6 +424,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "last_user",
"fieldtype": "Link",
"hidden": 0,
@ -440,16 +453,14 @@
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-03-08 15:13:01.379471",
"modified": "2019-04-16 17:46:04.890120",
"modified_by": "Administrator",
"module": "Automation",
"name": "Assignment Rule",
@ -478,7 +489,6 @@
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",

View file

@ -7,13 +7,14 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.desk.form import assign_to
import frappe.cache_manager
class AssignmentRule(Document):
def on_update(self): # pylint: disable=no-self-use
frappe.cache().delete_value('assignment_rule')
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.name)
def after_rename(self): # pylint: disable=no-self-use
frappe.cache().delete_value('assignment_rule')
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.name)
def apply_unassign(self, doc, assignments):
if (self.unassign_condition and
@ -113,14 +114,14 @@ def apply(doc, method):
if frappe.flags.in_patch or frappe.flags.in_install:
return
assignment_rules = frappe.cache().get_value('assignment_rule', get_assignment_rules)
assignment_rules = frappe.cache_manager.get_doctype_map('Assignment Rule', doc.doctype, dict(
document_type = doc.doctype, disabled = 0), order_by = 'priority desc')
assignment_rule_docs = []
# build rules
if doc.doctype in assignment_rules:
# multiple auto assigns
for d in frappe.db.get_all('Assignment Rule', dict(document_type=doc.doctype, disabled = 0), order_by = 'priority desc'):
assignment_rule_docs.append(frappe.get_doc('Assignment Rule', d.name))
# multiple auto assigns
for d in assignment_rules:
assignment_rule_docs.append(frappe.get_doc('Assignment Rule', d.name))
if not assignment_rule_docs:
return

View file

@ -0,0 +1,8 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Milestone', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,230 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "",
"beta": 0,
"creation": "2019-04-17 09:39:15.647817",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 0,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "reference_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Document Type",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "reference_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Document",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "track_field",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Track Field",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "value",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Value",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "milestone_tracker",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Milestone Tracker",
"length": 0,
"no_copy": 0,
"options": "Milestone Tracker",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_toolbar": 0,
"idx": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-04-17 16:01:21.430344",
"modified_by": "Administrator",
"module": "Automation",
"name": "Milestone",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "ASC",
"title_field": "reference_type",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View file

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class Milestone(Document):
pass
def on_doctype_update():
frappe.db.add_index("Milestone", ["reference_type", "reference_name"])

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
#import frappe
import unittest
class TestMilestone(unittest.TestCase):
pass

View file

@ -0,0 +1,33 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Milestone Tracker', {
refresh: function(frm) {
frm.trigger('update_options');
},
document_type: function(frm) {
frm.trigger('update_options');
},
update_options: function(frm) {
// update select options for `track_field`
let doctype = frm.doc.document_type;
let track_fields = [];
if (doctype) {
frappe.model.with_doctype(doctype, () => {
// get all date and datetime fields
frappe.get_meta(doctype).fields.map(df => {
if (['Link', 'Select'].includes(df.fieldtype)) {
track_fields.push({label: df.label, value: df.fieldname});
}
});
frm.set_df_property('track_field', 'options', track_fields);
});
} else {
// update select options
frm.set_df_property('track_field', 'options', []);
}
},
});

View file

@ -0,0 +1,162 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "format:{document_type}-{track_field}",
"beta": 0,
"creation": "2019-04-17 09:36:41.774774",
"custom": 0,
"description": "Track milestones for any document",
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 0,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "document_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Document Type to Track",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "track_field",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Field to Track",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "disabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Disabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_toolbar": 0,
"idx": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-04-22 16:03:32.848937",
"modified_by": "Administrator",
"module": "Automation",
"name": "Milestone Tracker",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View file

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
import frappe.cache_manager
class MilestoneTracker(Document):
def on_update(self):
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.name)
def on_trash(self):
frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.name)
def apply(self, doc):
before_save = doc.get_doc_before_save()
from_value = before_save and before_save.get(self.track_field) or None
if from_value != doc.get(self.track_field):
frappe.get_doc(dict(
doctype = 'Milestone',
reference_type = doc.doctype,
reference_name = doc.name,
track_field = self.track_field,
from_value = from_value,
value = doc.get(self.track_field),
milestone_tracker = self.name,
)).insert(ignore_permissions=True)
def evaluate_milestone(doc, event):
for d in frappe.cache_manager.get_doctype_map('Milestone Tracker', doc.doctype,
dict(document_type = doc.doctype, disabled=0)):
frappe.get_doc('Milestone Tracker', d.name).apply(doc)

View file

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
class TestMilestoneTracker(unittest.TestCase):
def test_milestone(self):
frappe.db.sql('delete from `tabMilestone Tracker`')
frappe.get_doc(dict(
doctype = 'Milestone Tracker',
document_type = 'ToDo',
track_field = 'status'
)).insert()
todo = frappe.get_doc(dict(
doctype = 'ToDo',
description = 'test milestone'
)).insert()
milestones = frappe.get_all('Milestone',
fields = ['track_field', 'value', 'milestone_tracker'],
filters = dict(reference_type = todo.doctype, reference_name=todo.name))
self.assertEqual(len(milestones), 1)
self.assertEqual(milestones[0].track_field, 'status')
self.assertEqual(milestones[0].value, 'Open')
todo.status = 'Closed'
todo.save()
milestones = frappe.get_all('Milestone',
fields = ['track_field', 'value', 'milestone_tracker'],
filters = dict(reference_type = todo.doctype, reference_name=todo.name),
order_by = 'modified desc')
self.assertEqual(len(milestones), 2)
self.assertEqual(milestones[0].track_field, 'status')
self.assertEqual(milestones[0].value, 'Closed')

View file

@ -16,8 +16,9 @@ from frappe.desk.form.load import get_meta_bundle
from frappe.utils.change_log import get_versions
from frappe.translate import get_lang_dict
from frappe.email.inbox import get_email_accounts
from frappe.core.doctype.feedback_trigger.feedback_trigger import get_enabled_feedback_trigger
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
from frappe.social.doctype.post.post import frequently_visited_links
def get_bootinfo():
"""build and return boot info"""
@ -75,11 +76,12 @@ def get_bootinfo():
bootinfo.calendars = sorted(frappe.get_hooks("calendars"))
bootinfo.treeviews = frappe.get_hooks("treeviews") or []
bootinfo.lang_dict = get_lang_dict()
bootinfo.feedback_triggers = get_enabled_feedback_trigger()
bootinfo.gsuite_enabled = get_gsuite_status()
bootinfo.success_action = get_success_action()
bootinfo.update(get_email_accounts(user=frappe.session.user))
bootinfo.energy_points_enabled = is_energy_point_enabled()
bootinfo.points = get_energy_points(frappe.session.user)
bootinfo.frequently_visited_links = frequently_visited_links()
return bootinfo

View file

@ -3,31 +3,40 @@
from __future__ import unicode_literals
import frappe
import frappe, json
import frappe.defaults
from frappe.desk.notifications import (delete_notification_count_for,
clear_notifications)
common_default_keys = ["__default", "__global"]
def clear_user_cache(user=None):
cache = frappe.cache()
global_cache_keys = ("app_hooks", "installed_apps",
"app_modules", "module_app", "notification_config", 'system_settings',
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
'active_modules', 'assignment_rule')
groups = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
"defaults", "user_permissions", "home_page", "linked_with",
"desktop_icons", 'portal_menu_items')
doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified",
"linked_doctypes", 'notifications', 'workflow' ,'energy_point_rule_map')
def clear_user_cache(user=None):
cache = frappe.cache()
# this will automatically reload the global cache
# so it is important to clear this first
clear_notifications(user)
if user:
for name in groups:
for name in user_cache_keys:
cache.hdel(name, user)
cache.delete_keys("user:" + user)
clear_defaults_cache(user)
else:
for name in groups:
for name in user_cache_keys:
cache.delete_key(name)
clear_defaults_cache()
clear_global_cache()
@ -37,10 +46,7 @@ def clear_global_cache():
clear_doctype_cache()
clear_website_cache()
frappe.cache().delete_value(["app_hooks", "installed_apps",
"app_modules", "module_app", "notification_config", 'system_settings',
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
'active_modules', 'assignment_rule'])
frappe.cache().delete_value(global_cache_keys)
frappe.setup_module_map()
def clear_defaults_cache(user=None):
@ -63,11 +69,8 @@ def clear_doctype_cache(doctype=None):
for key in ('is_table', 'doctype_modules'):
cache.delete_value(key)
groups = ["meta", "form_meta", "table_columns", "last_modified",
"linked_doctypes", 'notifications', 'workflow']
def clear_single(dt):
for name in groups:
for name in doctype_cache_keys:
cache.hdel(name, dt)
if doctype:
@ -84,9 +87,31 @@ def clear_doctype_cache(doctype=None):
else:
# clear all
for name in groups:
for name in doctype_cache_keys:
cache.delete_value(name)
# Clear all document's cache. To clear documents of a specific DocType document_cache should be restructured
clear_document_cache()
def get_doctype_map(doctype, name, filters, order_by=None):
cache = frappe.cache()
cache_key = frappe.scrub(doctype) + '_map'
doctype_map = cache.hget(cache_key, name)
if doctype_map:
# cached, return
items = json.loads(doctype_map)
else:
# non cached, build cache
try:
items = frappe.get_all(doctype, filters=filters, order_by = order_by)
cache.hset(cache_key, doctype, json.dumps(items))
except frappe.db.TableMissingError:
# executed from inside patch, ignore
items = []
return items
def clear_doctype_map(doctype, name):
cache_key = frappe.scrub(doctype) + '_map'
frappe.cache().hdel(cache_key, name)

View file

@ -17,10 +17,6 @@ from frappe.chat.util import (
session = frappe.session
class ChatProfile(Document):
def before_save(self):
if not self.is_new():
self.get_doc_before_save()
def on_update(self):
if not self.is_new():
b, a = self.get_doc_before_save(), self

View file

@ -69,10 +69,6 @@ class ChatRoom(Document):
if self.type == "Group" and not self.room_name:
frappe.throw(_('Group name cannot be empty.'))
def before_save(self):
if not self.is_new():
self.get_doc_before_save()
def on_update(self):
if not self.is_new():
before = self.get_doc_before_save()

View file

@ -371,4 +371,5 @@ def check_parent_permission(parent, child_doctype):
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
raise frappe.PermissionError

View file

@ -157,16 +157,17 @@ def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_ro
admin_password=admin_password)
@click.command('install-app')
@click.argument('app')
@click.argument('apps', nargs=-1)
@pass_context
def install_app(context, app):
"Install a new app to site"
def install_app(context, apps):
"Install a new app to site, supports multiple apps"
from frappe.installer import install_app as _install_app
for site in context.sites:
frappe.init(site=site)
frappe.connect()
try:
_install_app(app, verbose=context.verbose)
for app in apps:
_install_app(app, verbose=context.verbose)
finally:
frappe.destroy()

View file

@ -625,19 +625,30 @@ def setup_help(context):
print_in_app_help_deprecation()
@click.command('rebuild-global-search')
@click.option('--static-pages', is_flag=True, default=False, help='Rebuild global search for static pages')
@pass_context
def rebuild_global_search(context):
def rebuild_global_search(context, static_pages=False):
'''Setup help table in the current site (called after migrate)'''
from frappe.utils.global_search import (get_doctypes_with_global_search, rebuild_for_doctype)
from frappe.utils.global_search import (get_doctypes_with_global_search, rebuild_for_doctype,
get_routes_to_index, add_route_to_global_search, sync_global_search)
for site in context.sites:
try:
frappe.init(site)
frappe.connect()
doctypes = get_doctypes_with_global_search()
for i, doctype in enumerate(doctypes):
rebuild_for_doctype(doctype)
update_progress_bar('Rebuilding Global Search', i, len(doctypes))
if static_pages:
routes = get_routes_to_index()
for i, route in enumerate(routes):
add_route_to_global_search(route)
frappe.local.request = None
update_progress_bar('Rebuilding Global Search', i, len(routes))
sync_global_search()
else:
doctypes = get_doctypes_with_global_search()
for i, doctype in enumerate(doctypes):
rebuild_for_doctype(doctype)
update_progress_bar('Rebuilding Global Search', i, len(doctypes))
finally:
frappe.destroy()

View file

@ -78,15 +78,22 @@ def get_modules_from_app(app):
return active_modules_list
def get_all_empty_tables_by_module():
results = frappe.db.sql("""
SELECT
name, module
FROM information_schema.tables as i
JOIN tabDocType as d
ON i.table_name = CONCAT('tab', d.name)
WHERE table_rows = 0;
""")
results = frappe.db.multisql({
'mariadb': '''
SELECT `name`, `module`
FROM information_schema.tables AS i
JOIN `tabDocType` AS d
ON i.table_name = CONCAT('tab', d.name)
WHERE `table_rows` = 0;
''',
'postgres': '''
SELECT "name", "module"
FROM "pg_stat_all_tables" AS i
JOIN "tabDocType" AS d
ON i.relname = CONCAT('tab', d.name)
WHERE n_tup_ins = 0;
'''
})
empty_tables_by_module = {}
@ -95,7 +102,6 @@ def get_all_empty_tables_by_module():
empty_tables_by_module[module].append(doctype)
else:
empty_tables_by_module[module] = [doctype]
return empty_tables_by_module
def is_domain(module):

View file

@ -152,3 +152,11 @@ def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, fil
valid_doctypes = [[doctype] for doctype in valid_doctypes]
return valid_doctypes
def set_link_title(doc):
if not doc.links:
return
for link in doc.links:
if not link.link_title:
linked_doc = frappe.get_doc(link.link_doctype, link.link_name)
link.link_title = linked_doc.get("title_field") or linked_doc.get("name")

View file

@ -15,6 +15,7 @@ from frappe.model.naming import make_autoname
from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
from six import iteritems, string_types
from past.builtins import cmp
from frappe.contacts.address_and_contact import set_link_title
import functools
@ -39,6 +40,7 @@ class Address(Document):
def validate(self):
self.link_address()
self.validate_reference()
set_link_title(self)
deduplicate_dynamic_links(self)
def link_address(self):

View file

@ -10,6 +10,7 @@ from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_li
from six import iteritems
from past.builtins import cmp
from frappe.model.naming import append_number_if_name_exists
from frappe.contacts.address_and_contact import set_link_title
import functools
@ -31,6 +32,7 @@ class Contact(Document):
if self.email_id:
self.email_id = self.email_id.strip()
self.set_user()
set_link_title(self)
if self.email_id and not self.image:
self.image = has_gravatar(self.email_id)

View file

@ -6,7 +6,7 @@ from __future__ import unicode_literals
from frappe import _
from frappe.utils import get_fullname, now
from frappe.model.document import Document
from frappe.core.utils import get_parent_doc, set_timeline_doc
from frappe.core.utils import set_timeline_doc
import frappe
class ActivityLog(Document):

View file

@ -67,7 +67,7 @@ def get_feed_match_conditions(user=None, doctype='Comment'):
user_permissions = frappe.permissions.get_user_permissions(user)
can_read = frappe.get_user().get_can_read()
can_read_doctypes = ['"{}"'.format(doctype) for doctype in
can_read_doctypes = ["'{}'".format(doctype) for doctype in
list(set(can_read) - set(list(user_permissions)))]
if can_read_doctypes:

View file

@ -48,10 +48,10 @@ class TestComment(unittest.TestCase):
add_comment('pleez vizits my site http://mysite.com', 'test@test.com', 'bad commentor',
'Blog Post', test_blog.name, test_blog.route)
self.assertEqual(frappe.get_all('Comment', fields = ['*'], filters = dict(
self.assertEqual(len(frappe.get_all('Comment', fields = ['*'], filters = dict(
reference_doctype = test_blog.doctype,
reference_name = test_blog.name
))[0].published, 0)
))), 0)

View file

@ -31,13 +31,6 @@ frappe.ui.form.on("Communication", {
}
}
if(frm.doc.communication_type == "Feedback") {
frm.add_custom_button(__("Resend"), function() {
var feedback = new frappe.utils.Feedback();
feedback.resend_feedback_request(frm.doc);
});
}
if(frm.doc.status==="Open") {
frm.add_custom_button(__("Close"), function() {
frm.set_value("status", "Closed");
@ -54,7 +47,7 @@ frappe.ui.form.on("Communication", {
frm.trigger('show_relink_dialog');
});
if(frm.doc.communication_type=="Communication"
if(frm.doc.communication_type=="Communication"
&& frm.doc.communication_medium == "Email"
&& frm.doc.sent_or_received == "Received") {
@ -90,7 +83,7 @@ frappe.ui.form.on("Communication", {
}
}
if(frm.doc.communication_type=="Communication"
if(frm.doc.communication_type=="Communication"
&& frm.doc.communication_medium == "Phone"
&& frm.doc.sent_or_received == "Received"){
@ -185,7 +178,7 @@ frappe.ui.form.on("Communication", {
forward_mail: function(frm) {
var args = frm.events.get_mail_args(frm)
$.extend(args, {
$.extend(args, {
forward: true,
subject: __("Fw: {0}", [frm.doc.subject]),
})

File diff suppressed because it is too large Load diff

View file

@ -8,11 +8,11 @@ from frappe.model.document import Document
from frappe.utils import validate_email_address, get_fullname, strip_html, cstr
from frappe.core.doctype.communication.email import (validate_email,
notify, _notify, update_parent_mins_to_first_response)
from frappe.core.utils import get_parent_doc, set_timeline_doc
from frappe.core.utils import get_parent_doc
from frappe.utils.bot import BotReply
from frappe.utils import parse_addr
from frappe.core.doctype.comment.comment import update_comment_in_doc
from email.utils import parseaddr
from collections import Counter
exclude_from_linked_with = True
@ -58,7 +58,10 @@ class Communication(Document):
self.set_sender_full_name()
validate_email(self)
set_timeline_doc(self)
if self.communication_medium == "Email":
self.set_timeline_links()
self.deduplicate_timeline_links()
def validate_reference(self):
if self.reference_doctype and self.reference_name:
@ -79,6 +82,7 @@ class Communication(Document):
circular_linking = True
break
doc = get_parent_doc(doc)
if circular_linking:
frappe.throw(_("Please make sure the Reference Communication Docs are not circularly linked."), frappe.CircularLinkingError)
@ -231,26 +235,66 @@ class Communication(Document):
if commit:
frappe.db.commit()
# Timeline Links
def set_timeline_links(self):
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc])
for contact_name in contacts:
self.add_link('Contact', contact_name)
#link contact's dynamic links to communication
add_contact_links_to_communication(self, contact_name)
def deduplicate_timeline_links(self):
if self.timeline_links:
links, duplicate = [], False
for l in self.timeline_links:
t = (l.link_doctype, l.link_name)
if not t in links:
links.append(t)
else:
duplicate = True
if duplicate:
del self.timeline_links[:] # make it python 2 compatible as list.clear() is python 3 only
for l in links:
self.add_link(link_doctype=l[0], link_name=l[1])
def add_link(self, link_doctype, link_name, autosave=False):
self.append("timeline_links",
{
"link_doctype": link_doctype,
"link_name": link_name
}
)
if autosave:
self.save(ignore_permissions=True)
def get_links(self):
return self.timeline_links
def remove_link(self, link_doctype, link_name, autosave=False, ignore_permissions=True):
for l in self.timeline_links:
if l.link_doctype == link_doctype and l.link_name == link_name:
self.timeline_links.remove(l)
if autosave:
self.save(ignore_permissions=ignore_permissions)
def on_doctype_update():
"""Add indexes in `tabCommunication`"""
frappe.db.add_index("Communication", ["reference_doctype", "reference_name"])
frappe.db.add_index("Communication", ["timeline_doctype", "timeline_name"])
frappe.db.add_index("Communication", ["link_doctype", "link_name"])
frappe.db.add_index("Communication", ["status", "communication_type"])
def has_permission(doc, ptype, user):
if ptype=="read":
if (doc.reference_doctype == "Communication" and doc.reference_name == doc.name) \
or (doc.timeline_doctype == "Communication" and doc.timeline_name == doc.name):
return
if doc.reference_doctype == "Communication" and doc.reference_name == doc.name:
return
if doc.reference_doctype and doc.reference_name:
if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name):
return True
if doc.timeline_doctype and doc.timeline_name:
if frappe.has_permission(doc.timeline_doctype, ptype="read", doc=doc.timeline_name):
return True
def get_permission_query_conditions_for_communication(user):
if not user: user = frappe.session.user
@ -265,8 +309,44 @@ def get_permission_query_conditions_for_communication(user):
distinct=True, order_by="idx")
if not accounts:
return """tabCommunication.communication_medium!='Email'"""
return """`tabCommunication`.communication_medium!='Email'"""
email_accounts = [ '"%s"'%account.get("email_account") for account in accounts ]
return """tabCommunication.email_account in ({email_accounts})"""\
return """`tabCommunication`.email_account in ({email_accounts})"""\
.format(email_accounts=','.join(email_accounts))
def get_contacts(email_strings):
email_addrs = []
for email_string in email_strings:
if email_string:
for email in email_string.split(","):
parsed_email = parseaddr(email)[1]
if parsed_email:
email_addrs.append(parsed_email)
contacts = []
for email in email_addrs:
contact_name = frappe.db.get_value('Contact', {'email_id': email})
if not contact_name:
contact = frappe.get_doc({
"doctype": "Contact",
"first_name": frappe.unscrub(email.split("@")[0]),
"email_id": email
}).insert(ignore_permissions=True)
contact_name = contact.name
contacts.append(contact_name)
return contacts
def add_contact_links_to_communication(communication, contact_name):
contact_links = frappe.get_list("Dynamic Link", filters={
"parenttype": "Contact",
"parent": contact_name
}, fields=["link_doctype", "link_name"])
if contact_links:
for contact_link in contact_links:
communication.add_link(contact_link.link_doctype, contact_link.link_name)

View file

@ -22,7 +22,7 @@ from frappe.utils.background_jobs import enqueue
def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
sender=None, sender_full_name=None, recipients=None, communication_medium="Email", send_email=False,
print_html=None, print_format=None, attachments='[]', send_me_a_copy=False, cc=None, bcc=None,
flags=None, read_receipt=None, print_letterhead=True):
flags=None, read_receipt=None, print_letterhead=True, email_template=None):
"""Make a new communication.
:param doctype: Reference DocType.
@ -38,6 +38,7 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
:param print_format: Print Format name of parent document to be sent as attachment.
:param attachments: List of attachments as list of files or JSON string.
:param send_me_a_copy: Send a copy to the sender (default **False**).
:param email_template: Template which is used to compose mail .
"""
is_error_report = (doctype=="User" and name==frappe.session.user and subject=="Error Report")
@ -66,15 +67,13 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
"sent_or_received": sent_or_received,
"reference_doctype": doctype,
"reference_name": name,
"email_template": email_template,
"message_id":get_message_id().strip(" <>"),
"read_receipt":read_receipt,
"has_attachment": 1 if attachments else 0
})
comm.insert(ignore_permissions=True)
}).insert(ignore_permissions=True)
if not doctype:
# if no reference given, then send it against the communication
comm.db_set(dict(reference_doctype='Communication', reference_name=comm.name))
comm.save(ignore_permissions=True)
if isinstance(attachments, string_types):
attachments = json.loads(attachments)
@ -555,5 +554,4 @@ def mark_email_as_seen(name=None):
frappe.response["type"] = 'binary'
frappe.response["filename"] = "imaginary_pixel.png"
frappe.response["filecontent"] = buffered_obj.getvalue()
frappe.response["filecontent"] = buffered_obj.getvalue()

View file

@ -44,28 +44,126 @@ class TestCommunication(unittest.TestCase):
self.assertFalse(frappe.utils.parse_addr(x)[0])
def test_circular_linking(self):
content = "This was created to test circular linking"
a = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": content,
}).insert()
"content": "This was created to test circular linking: Communication A",
}).insert(ignore_permissions=True)
b = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": content,
"content": "This was created to test circular linking: Communication B",
"reference_doctype": "Communication",
"reference_name": a.name
}).insert()
}).insert(ignore_permissions=True)
c = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": content,
"content": "This was created to test circular linking: Communication C",
"reference_doctype": "Communication",
"reference_name": b.name
}).insert()
}).insert(ignore_permissions=True)
a = frappe.get_doc("Communication", a.name)
a.reference_doctype = "Communication"
a.reference_name = c.name
self.assertRaises(frappe.CircularLinkingError, a.save)
def test_deduplication_timeline_links(self):
note = frappe.get_doc({
"doctype": "Note",
"title": "deduplication timeline links",
"content": "deduplication timeline links"
}).insert(ignore_permissions=True)
comm = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": "Deduplication of Links",
"communication_medium": "Email"
}).insert(ignore_permissions=True)
#adding same link twice
comm.add_link(link_doctype="Note", link_name=note.name, autosave=True)
comm.add_link(link_doctype="Note", link_name=note.name, autosave=True)
comm = frappe.get_doc("Communication", comm.name)
self.assertNotEqual(2, len(comm.timeline_links))
def test_contacts_attached(self):
contact_sender = frappe.get_doc({
"doctype": "Contact",
"first_name": frappe.generate_hash(length=10),
"email_id": "comm_sender@example.com"
}).insert(ignore_permissions=True)
contact_recipient = frappe.get_doc({
"doctype": "Contact",
"first_name": frappe.generate_hash(length=10),
"email_id": "comm_recipient@example.com"
}).insert(ignore_permissions=True)
contact_cc = frappe.get_doc({
"doctype": "Contact",
"first_name": frappe.generate_hash(length=10),
"email_id": "comm_cc@example.com"
}).insert(ignore_permissions=True)
comm = frappe.get_doc({
"doctype": "Communication",
"communication_medium": "Email",
"subject": "Contacts Attached Test",
"sender": "comm_sender@example.com",
"recipients": "comm_recipient@example.com",
"cc": "comm_cc@example.com"
}).insert(ignore_permissions=True)
comm = frappe.get_doc("Communication", comm.name)
contact_links = []
for timeline_link in comm.timeline_links:
contact_links.append(timeline_link.link_name)
self.assertIn(contact_sender.name, contact_links)
self.assertIn(contact_recipient.name, contact_links)
self.assertIn(contact_cc.name, contact_links)
def test_get_communication_data(self):
from frappe.desk.form.load import get_communication_data
note = frappe.get_doc({
"doctype": "Note",
"title": "get communication data",
"content": "get communication data"
}).insert(ignore_permissions=True)
comm_note_1 = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": "Test Get Communication Data 1",
"communication_medium": "Email"
}).insert(ignore_permissions=True)
comm_note_1.add_link(link_doctype="Note", link_name=note.name, autosave=True)
comm_note_2 = frappe.get_doc({
"doctype": "Communication",
"communication_type": "Communication",
"content": "Test Get Communication Data 2",
"communication_medium": "Email"
}).insert(ignore_permissions=True)
comm_note_2.add_link(link_doctype="Note", link_name=note.name, autosave=True)
comms = get_communication_data("Note", note.name, as_dict=True)
data = []
for comm in comms:
data.append(comm.name)
self.assertIn(comm_note_1.name, data)
self.assertIn(comm_note_2.name, data)

View file

@ -30,6 +30,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "",
"length": 0,
@ -62,6 +63,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Label",
"length": 0,
@ -99,13 +101,14 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Type",
"length": 0,
"no_copy": 0,
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
@ -134,6 +137,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Name",
"length": 0,
@ -168,6 +172,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Mandatory",
"length": 0,
@ -206,6 +211,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Precision",
"length": 0,
@ -240,6 +246,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Length",
"length": 0,
@ -273,6 +280,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Index",
"length": 0,
@ -309,6 +317,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "In List View",
"length": 0,
@ -343,6 +352,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "In Standard Filter",
"length": 0,
@ -377,6 +387,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "In Global Search",
"length": 0,
@ -394,6 +405,40 @@
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "in_preview",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "In Preview",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
@ -410,6 +455,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Allow in Quick Entry",
"length": 0,
@ -443,6 +489,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Bold",
"length": 0,
@ -478,6 +525,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Translatable",
"length": 0,
@ -512,6 +560,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Collapsible",
"length": 255,
@ -546,6 +595,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Collapsible Depends On",
"length": 0,
@ -580,6 +630,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
@ -612,6 +663,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Options",
"length": 0,
@ -646,6 +698,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Default",
"length": 0,
@ -680,6 +733,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Fetch From",
"length": 0,
@ -714,6 +768,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Fetch If Empty",
"length": 0,
@ -747,6 +802,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Permissions",
"length": 0,
@ -779,6 +835,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Display Depends On",
"length": 255,
@ -814,6 +871,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Hidden",
"length": 0,
@ -850,6 +908,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Read Only",
"length": 0,
@ -884,6 +943,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Unique",
"length": 0,
@ -918,6 +978,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Set Only Once",
"length": 0,
@ -951,6 +1012,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Allow Bulk Edit",
"length": 0,
@ -984,6 +1046,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
@ -1016,6 +1079,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Perm Level",
"length": 0,
@ -1053,6 +1117,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Ignore User Permissions",
"length": 0,
@ -1086,6 +1151,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Allow on Submit",
"length": 0,
@ -1122,6 +1188,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Report Hide",
"length": 0,
@ -1159,6 +1226,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Remember Last Selected Value",
"length": 0,
@ -1193,6 +1261,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Ignore XSS Filter",
"length": 0,
@ -1226,6 +1295,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Display",
"length": 0,
@ -1258,6 +1328,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "In Filter",
"length": 0,
@ -1294,6 +1365,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "No Copy",
"length": 0,
@ -1330,6 +1402,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Print Hide",
"length": 0,
@ -1367,6 +1440,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Print Hide If No Value",
"length": 0,
@ -1400,6 +1474,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Print Width",
"length": 0,
@ -1432,6 +1507,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Width",
"length": 0,
@ -1470,6 +1546,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Columns",
"length": 0,
@ -1503,6 +1580,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
@ -1534,6 +1612,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_preview": 0,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
@ -1570,6 +1649,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
@ -1603,6 +1683,7 @@
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
@ -1622,16 +1703,14 @@
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2019-03-18 17:59:57.873790",
"modified": "2019-04-08 12:19:53.415372",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",
@ -1639,7 +1718,6 @@
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_order": "ASC",
"track_changes": 0,

View file

@ -3,7 +3,7 @@
# See license.txt
from __future__ import unicode_literals
import frappe
# import frappe
import unittest
class Test{classname}(unittest.TestCase):

File diff suppressed because it is too large Load diff

View file

@ -17,7 +17,10 @@ from frappe.desk.notifications import delete_notification_count_for
from frappe.modules import make_boilerplate, get_doc_path
from frappe.database.schema import validate_column_name, validate_column_length
from frappe.model.docfield import supports_translation
from frappe.modules.import_file import get_file_path
from six import iteritems
import frappe.website.render
import json
class InvalidFieldNameError(frappe.ValidationError): pass
@ -252,7 +255,8 @@ class DocType(Document):
self.update_fields_to_fetch()
from frappe import conf
if not self.custom and not (frappe.flags.in_import or frappe.flags.in_test) and conf.get('developer_mode'):
allow_doctype_export = frappe.flags.allow_doctype_export or (not frappe.flags.in_test and conf.get('developer_mode'))
if not self.custom and not frappe.flags.in_import and allow_doctype_export:
self.export_doc()
self.make_controller_template()
@ -343,7 +347,8 @@ class DocType(Document):
if merge:
frappe.throw(_("DocType can not be merged"))
if not frappe.flags.in_test and not frappe.flags.in_patch:
# Do not rename and move files and folders for custom doctype
if not self.custom and not frappe.flags.in_test and not frappe.flags.in_patch:
self.rename_files_and_folders(old, new)
def after_rename(self, old, new, merge=False):
@ -403,6 +408,72 @@ class DocType(Document):
if naming_series[0].default:
make_property_setter(self.name, "naming_series", "default", naming_series[0].default, "Text", validate_fields_for_doctype=False)
def before_export(self, docdict):
# remove null and empty fields
def remove_null_fields(o):
to_remove = []
for attr, value in iteritems(o):
if isinstance(value, list):
for v in value:
remove_null_fields(v)
elif not value:
to_remove.append(attr)
for attr in to_remove:
del o[attr]
remove_null_fields(docdict)
# retain order of 'fields' table and change order in 'field_order'
docdict["field_order"] = [f.fieldname for f in self.fields]
path = get_file_path(self.module, "DocType", self.name)
if os.path.exists(path):
try:
with open(path, 'r') as txtfile:
olddoc = json.loads(txtfile.read())
old_field_names = [f['fieldname'] for f in olddoc.get("fields", [])]
if old_field_names:
new_field_dicts = []
remaining_field_names = [f.fieldname for f in self.fields]
for fieldname in old_field_names:
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict['fields']))
if field_dict:
new_field_dicts.append(field_dict[0])
remaining_field_names.remove(fieldname)
for fieldname in remaining_field_names:
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict['fields']))
new_field_dicts.append(field_dict[0])
docdict['fields'] = new_field_dicts
except ValueError:
pass
@staticmethod
def prepare_for_import(docdict):
# set order of fields from field_order
if docdict.get("field_order"):
new_field_dicts = []
remaining_field_names = [f['fieldname'] for f in docdict.get('fields', [])]
for fieldname in docdict.get('field_order'):
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict.get('fields', [])))
if field_dict:
new_field_dicts.append(field_dict[0])
remaining_field_names.remove(fieldname)
for fieldname in remaining_field_names:
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict.get('fields', [])))
new_field_dicts.append(field_dict[0])
docdict['fields'] = new_field_dicts
if "field_order" in docdict:
del docdict["field_order"]
def export_doc(self):
"""Export to standard folder `[module]/doctype/[name]/[name].json`."""
from frappe.modules.export_file import export_to_files
@ -545,7 +616,9 @@ def validate_fields(meta):
frappe.throw(_("Options 'Dynamic Link' type of field must point to another Link Field with options as 'DocType'"))
def check_illegal_default(d):
if d.fieldtype == "Check" and d.default and d.default not in ('0', '1'):
if d.fieldtype == "Check" and not d.default:
d.default = '0'
if d.fieldtype == "Check" and d.default not in ('0', '1'):
frappe.throw(_("Default for 'Check' type of field must be either '0' or '1'"))
if d.fieldtype == "Select" and d.default and (d.default not in d.options.split("\n")):
frappe.throw(_("Default for {0} must be an option").format(d.fieldname))

View file

@ -104,4 +104,125 @@ class TestDocType(unittest.TestCase):
for depends_on in ["depends_on", "collapsible_depends_on"]:
condition = field.get(depends_on)
if condition:
self.assertFalse(re.match(pattern, condition))
self.assertFalse(re.match(pattern, condition))
def test_sync_field_order(self):
from frappe.modules.import_file import get_file_path
import os
# create test doctype
test_doctype = frappe.get_doc({
"doctype": "DocType",
"module": "Core",
"fields": [
{
"label": "Field 1",
"fieldname": "field_1",
"fieldtype": "Data"
},
{
"label": "Field 2",
"fieldname": "field_2",
"fieldtype": "Data"
},
{
"label": "Field 3",
"fieldname": "field_3",
"fieldtype": "Data"
},
{
"label": "Field 4",
"fieldname": "field_4",
"fieldtype": "Data"
}
],
"permissions": [{
"role": "System Manager",
"read": 1
}],
"name": "Test Field Order DocType",
"__islocal": 1
})
path = get_file_path(test_doctype.module, test_doctype.doctype, test_doctype.name)
initial_fields_order = ['field_1', 'field_2', 'field_3', 'field_4']
frappe.delete_doc_if_exists("DocType", "Test Field Order DocType")
if os.path.isfile(path):
os.remove(path)
try:
frappe.flags.allow_doctype_export = 1
test_doctype.save()
# assert that field_order list is being created with the default order
test_doctype_json = frappe.get_file_json(path)
self.assertTrue(test_doctype_json.get("field_order"))
self.assertEqual(len(test_doctype_json['fields']), len(test_doctype_json['field_order']))
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], test_doctype_json['field_order'])
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order)
self.assertListEqual(test_doctype_json['field_order'], initial_fields_order)
# remove field_order to test reload_doc/sync/migrate is backwards compatible without field_order
del test_doctype_json['field_order']
with open(path, 'w+') as txtfile:
txtfile.write(frappe.as_json(test_doctype_json))
# assert that field_order is actually removed from the json file
test_doctype_json = frappe.get_file_json(path)
self.assertFalse(test_doctype_json.get("field_order"))
# make sure that migrate/sync is backwards compatible without field_order
frappe.reload_doctype(test_doctype.name, force=True)
test_doctype.reload()
# assert that field_order list is being created with the default order again
test_doctype.save()
test_doctype_json = frappe.get_file_json(path)
self.assertTrue(test_doctype_json.get("field_order"))
self.assertEqual(len(test_doctype_json['fields']), len(test_doctype_json['field_order']))
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], test_doctype_json['field_order'])
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order)
self.assertListEqual(test_doctype_json['field_order'], initial_fields_order)
# reorder fields: swap row 1 and 3
test_doctype.fields[0], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[0]
for i, f in enumerate(test_doctype.fields):
f.idx = i + 1
# assert that reordering fields only affects `field_order` rather than `fields` attr
test_doctype.save()
test_doctype_json = frappe.get_file_json(path)
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], initial_fields_order)
self.assertListEqual(test_doctype_json['field_order'], ['field_3', 'field_2', 'field_1', 'field_4'])
# reorder `field_order` in the json file: swap row 2 and 4
test_doctype_json['field_order'][1], test_doctype_json['field_order'][3] = test_doctype_json['field_order'][3], test_doctype_json['field_order'][1]
with open(path, 'w+') as txtfile:
txtfile.write(frappe.as_json(test_doctype_json))
# assert that reordering `field_order` from json file is reflected in DocType upon migrate/sync
frappe.reload_doctype(test_doctype.name, force=True)
test_doctype.reload()
self.assertListEqual([f.fieldname for f in test_doctype.fields], ['field_3', 'field_4', 'field_1', 'field_2'])
# insert row in the middle and remove first row (field 3)
test_doctype.append("fields", {
"label": "Field 5",
"fieldname": "field_5",
"fieldtype": "Data"
})
test_doctype.fields[4], test_doctype.fields[3] = test_doctype.fields[3], test_doctype.fields[4]
test_doctype.fields[3], test_doctype.fields[2] = test_doctype.fields[2], test_doctype.fields[3]
test_doctype.remove(test_doctype.fields[0])
for i, f in enumerate(test_doctype.fields):
f.idx = i + 1
test_doctype.save()
test_doctype_json = frappe.get_file_json(path)
self.assertListEqual([f['fieldname'] for f in test_doctype_json['fields']], ['field_1', 'field_2', 'field_4', 'field_5'])
self.assertListEqual(test_doctype_json['field_order'], ['field_4', 'field_5', 'field_1', 'field_2'])
except:
raise
finally:
frappe.flags.allow_doctype_export = 0

View file

@ -1,125 +1,47 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-01-13 04:55:18.835023",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"creation": "2017-01-13 04:55:18.835023",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"link_doctype",
"link_name",
"link_title"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "link_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Link DocType",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "link_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Link DocType",
"options": "DocType",
"reqd": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "link_name",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Link Name",
"length": 0,
"no_copy": 0,
"options": "link_doctype",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "link_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Link Name",
"options": "link_doctype",
"reqd": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "link_title",
"fieldtype": "Read Only",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Link Title",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "link_title",
"fieldtype": "Read Only",
"in_list_view": 1,
"label": "Link Title",
"read_only": 1
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-01-17 14:25:49.140730",
"modified_by": "Administrator",
"module": "Core",
"name": "Dynamic Link",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
],
"istable": 1,
"modified": "2019-05-16 19:54:31.400026",
"modified_by": "Administrator",
"module": "Core",
"name": "Dynamic Link",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -1,21 +0,0 @@
// Copyright (c) 2016, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Feedback Request', {
refresh: function(frm) {
var rating_icons = frappe.render_template("rating_icons", {rating: frm.doc.rating, show_label: false});
$(frm.fields_dict.feedback_rating.wrapper).html(rating_icons);
if(frm.doc.reference_doctype && frm.doc.reference_name) {
frm.add_custom_button(__(frm.doc.reference_name), function() {
frappe.set_route("Form", frm.doc.reference_doctype, frm.doc.reference_name);
});
}
if(frm.doc.reference_communication){
frm.add_custom_button(__("Communication"), function() {
frappe.set_route("Form", "Communication", frm.doc.reference_communication);
});
}
}
});

View file

@ -1,470 +0,0 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "",
"beta": 0,
"creation": "2017-01-27 15:43:33.780808",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "is_sent",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Sent",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "is_feedback_submitted",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Feedback Submitted",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Is Feedback request triggered manually ?",
"fieldname": "is_manual",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Manual",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 1,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "key",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Key",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reference",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"fieldname": "reference_doctype",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Reference DocType",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"fieldname": "reference_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Reference Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_5",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "feedback_trigger",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Feedback Trigger",
"length": 0,
"no_copy": 0,
"options": "Feedback Trigger",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": "eval: doc.rating",
"columns": 0,
"fieldname": "section_break_1",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Feedback Rating",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"fieldname": "rating",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Rating",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "feedback_rating",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Feedback Rating",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference_communication",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reference Communication",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-03-03 08:11:09.718589",
"modified_by": "Administrator",
"module": "Core",
"name": "Feedback Request",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 0
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "reference_name",
"track_changes": 1,
"track_seen": 0
}

View file

@ -1,38 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils import get_datetime
class FeedbackRequest(Document):
def autoname(self):
""" feedback request name in the format Feedback for {doctype} {name} on {datetime}"""
self.name = "Feedback for {doctype} {docname} on {datetime}".format(
doctype=self.reference_doctype,
docname=self.reference_name,
datetime=get_datetime()
)
def before_insert(self):
from frappe.utils import random_string
self.key = random_string(32)
@frappe.whitelist(allow_guest=True)
def is_valid_feedback_request(key=None):
if not key:
return False
is_feedback_submitted = frappe.db.get_value("Feedback Request", { "key": key }, "is_feedback_submitted")
if is_feedback_submitted:
return False
else:
return True
def delete_feedback_request():
""" clear 100 days old feedback request """
frappe.db.sql("""delete from `tabFeedback Request` where `creation` < (NOW() - INTERVAL '100' DAY)""")

View file

@ -1,16 +0,0 @@
frappe.listview_settings['Feedback Request'] = {
colwidths: {
subject: 2,
},
column_render: {
rating: function(doc) {
var html = ""
for (var i = 0; i < 5; i++) {
html += repl("<span class='indicator %(color)s'></span>",
{color: i<doc.rating? "blue": "darkgrey"})
}
return html;
}
}
}

View file

@ -1,12 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
# test_records = frappe.get_test_records('Feedback Request')
class TestFeedbackRequest(unittest.TestCase):
pass

View file

@ -1,50 +0,0 @@
// Copyright (c) 2016, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Feedback Trigger', {
onload: function(frm) {
frm.set_query("document_type", function() {
return {
"filters": {
"istable": 0
}
}
})
},
refresh: function(frm) {
frm.events.setup_field_options(frm);
},
document_type: function(frm) {
frm.set_value('email_field', '');
frm.set_value('email_fieldname');
frm.events.setup_field_options(frm);
},
email_field: function(frm) {
frm.set_value('email_fieldname', frm.fieldname_mapper[frm.doc.email_field]);
},
setup_field_options: function(frm) {
frm.fieldname_mapper = {};
frm.options = [];
if(!frm.doc.document_type)
return
frappe.model.with_doctype(frm.doc.document_type, function() {
var fields = frappe.get_doc("DocType", frm.doc.document_type).fields;
$.each(fields, function(idx, field) {
if(!in_list(frappe.model.no_value_type, field.fieldtype) && field.options == "Email") {
frm.options.push(field.label);
frm.fieldname_mapper[field.label] = field.fieldname;
}
})
frm.set_df_property("email_field", "options", [""].concat(frm.options));
frm.refresh_fields();
});
}
});

View file

@ -1,517 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:document_type",
"beta": 0,
"creation": "2017-01-24 15:46:38.366213",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "enabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Enabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "document_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Document Type",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval: doc.document_type",
"fieldname": "email_field",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Email Field",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "email_fieldname",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Email Fieldname",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_5",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "To add dynamic subject, use jinja tags like\n\n<div><pre><code>{{ doc.name }} Delivered</code></pre></div>",
"fieldname": "subject",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Subject",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"description": "Send Feedback Request only if there is at least one communication is available for the document.",
"fieldname": "check_communication",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Check Communication",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Optional: The alert will be sent if this expression is true",
"fieldname": "condition",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Condition",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_7",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "html_8",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"options": "<p><strong>Condition Examples:</strong></p>\n<pre>doc.status==\"Closed\"\ndoc.due_date==nowdate()\ndoc.total &gt; 40000\n</pre>",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Message",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "message",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Message",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "example",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Example",
"length": 0,
"no_copy": 0,
"options": "<h5>Message Example</h5>\n\n<pre>&lt;h3&gt;Issue Resolved&lt;/h3&gt;\n\n&lt;p&gt;Issue {{ doc.name }} Is resolved. Please check and confirm the same.&lt;/p&gt;\n\n&lt;p&gt; Your Feedback is important for us. Please give us your Feedback for {{ doc.name }}&lt;/p&gt;\n\n&lt;h4&gt;Details&lt;/h4&gt;</pre>",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-05-29 16:36:04.178592",
"modified_by": "Administrator",
"module": "Core",
"name": "Feedback Trigger",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "document_type",
"track_changes": 1,
"track_seen": 0
}

View file

@ -1,215 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import json
import frappe
from frappe import _
from frappe.utils import get_url
from frappe.model.document import Document
from frappe.utils.jinja import validate_template
class FeedbackTrigger(Document):
def validate(self):
frappe.cache().delete_value('feedback_triggers')
validate_template(self.subject)
validate_template(self.message)
self.validate_condition()
def on_trash(self):
frappe.cache().delete_value('feedback_triggers')
def validate_condition(self):
temp_doc = frappe.new_doc(self.document_type)
if self.condition:
try:
frappe.safe_eval(self.condition, None, get_context(temp_doc))
except:
frappe.throw(_("The condition '{0}' is invalid").format(self.condition))
def trigger_feedback_request(doc, method):
"""Trigger the feedback alert, or delete feedback requests on delete"""
def _get():
triggers = {}
if not (frappe.flags.in_migrate or frappe.flags.in_install):
for d in frappe.get_all('Feedback Trigger', dict(enabled=1), ['name', 'document_type']):
triggers[d.document_type] = d.name
return triggers
feedback_triggers = frappe.cache().get_value('feedback_triggers', _get)
if doc.doctype in feedback_triggers:
if doc.flags.in_delete:
frappe.enqueue('frappe.core.doctype.feedback_trigger.feedback_trigger.delete_feedback_request_and_feedback',
reference_doctype=doc.doctype, reference_name=doc.name, now=frappe.flags.in_test)
else:
frappe.enqueue('frappe.core.doctype.feedback_trigger.feedback_trigger.send_feedback_request',
trigger=feedback_triggers[doc.doctype], reference_doctype=doc.doctype,
reference_name=doc.name, now=frappe.flags.in_test)
@frappe.whitelist()
def send_feedback_request(reference_doctype, reference_name, trigger="Manual", details=None, is_manual=False):
""" send feedback alert """
if is_feedback_request_already_sent(reference_doctype, reference_name, is_manual=is_manual):
frappe.msgprint(_("Feedback Request is already sent to user"))
return None
details = json.loads(details) if details else \
get_feedback_request_details(reference_doctype, reference_name, trigger=trigger)
if not details:
return None
feedback_request, url = get_feedback_request_url(reference_doctype,
reference_name, details.get("recipients"), trigger)
feedback_msg = frappe.render_template("templates/emails/feedback_request_url.html", { "url": url })
# appending feedback url to message body
message = "{message}{feedback_msg}".format(
message=details.get("message"),
feedback_msg=feedback_msg
)
details.update({
"message": message,
"header": [details.get('subject'), 'blue']
})
if details:
frappe.sendmail(**details)
frappe.db.set_value("Feedback Request", feedback_request, "is_sent", 1)
@frappe.whitelist()
def get_feedback_request_details(reference_doctype, reference_name, trigger="Manual", request=None):
if not frappe.db.get_value(reference_doctype, reference_name):
# reference document is either deleted or renamed
return
elif not trigger and not request and not frappe.db.get_value("Feedback Trigger", { "document_type": reference_doctype }):
return
elif not trigger and request:
trigger = frappe.db.get_value("Feedback Request", request, "feedback_trigger")
else:
trigger = frappe.db.get_value("Feedback Trigger", { "document_type": reference_doctype })
if not trigger:
return
feedback_trigger = frappe.get_doc("Feedback Trigger", trigger)
doc = frappe.get_doc(reference_doctype, reference_name)
context = get_context(doc)
recipients = doc.get(feedback_trigger.email_fieldname, None)
if feedback_trigger.check_communication:
communications = frappe.get_all("Communication", filters={
"reference_doctype": reference_doctype,
"reference_name": reference_name,
"communication_type": "Communication",
"sent_or_received": "Sent"
}, fields=["name"])
if len(communications) < 1:
frappe.msgprint(_("At least one reply is mandatory before requesting feedback"))
return None
if recipients and (not feedback_trigger.condition or \
frappe.safe_eval(feedback_trigger.condition, None, context)):
subject = feedback_trigger.subject
context.update({ "feedback_trigger": feedback_trigger })
if "{" in subject:
subject = frappe.render_template(feedback_trigger.subject, context)
feedback_request_message = frappe.render_template(feedback_trigger.message, context)
return {
"subject": subject,
"recipients": recipients,
"reference_name":doc.name,
"reference_doctype":doc.doctype,
"message": feedback_request_message,
}
else:
frappe.msgprint(_("Feedback conditions do not match"))
return None
def get_feedback_request_url(reference_doctype, reference_name, recipients, trigger="Manual"):
""" prepare the feedback request url """
is_manual = 1 if trigger == "Manual" else 0
feedback_request = frappe.get_doc({
"is_manual": is_manual,
"feedback_trigger": trigger,
"doctype": "Feedback Request",
"reference_name": reference_name,
"reference_doctype": reference_doctype,
}).insert(ignore_permissions=True)
feedback_url = "{base_url}/feedback?reference_doctype={doctype}&reference_name={docname}&email={email_id}&key={nonce}".format(
base_url=get_url(),
doctype=reference_doctype,
docname=reference_name,
email_id=recipients,
nonce=feedback_request.key
)
return [ feedback_request.name, feedback_url ]
def is_feedback_request_already_sent(reference_doctype, reference_name, is_manual=False):
"""
check if feedback request mail is already sent but feedback is not submitted
to avoid sending multiple feedback request mail
"""
is_request_sent = False
filters = {
"is_sent": 1,
"reference_name": reference_name,
"is_manual": 1 if is_manual else 0,
"reference_doctype": reference_doctype
}
if is_manual:
filters.update({ "is_feedback_submitted": 0 })
feedback_request = frappe.get_all("Feedback Request", filters=filters, fields=["name"])
if feedback_request: is_request_sent = True
return is_request_sent
def get_enabled_feedback_trigger():
""" get mapper of all the enable feedback trigger """
triggers = frappe.get_all("Feedback Trigger", filters={"enabled": 1},
fields=["document_type", "name"], as_list=True)
triggers = { dt[0]: dt[1] for dt in triggers }
return triggers
def get_context(doc):
return { "doc": doc }
def delete_feedback_request_and_feedback(reference_doctype, reference_name):
""" delete all the feedback request and feedback communication """
if not all([reference_doctype, reference_name]):
return
feedback_requests = frappe.get_all("Feedback Request", filters={
"is_feedback_submitted": 0,
"reference_doctype": reference_doctype,
"reference_name": reference_name
})
communications = frappe.get_all("Communication", {
"communication_type": "Feedback",
"reference_doctype": reference_doctype,
"reference_name": reference_name
})
for request in feedback_requests:
frappe.delete_doc("Feedback Request", request.get("name"), ignore_permissions=True)
for communication in communications:
frappe.delete_doc("Communication", communication.get("name"), ignore_permissions=True)

View file

@ -1,137 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
# test_records = frappe.get_test_records('Feedback Trigger')
def get_feedback_request(todo, feedback_trigger):
return frappe.db.get_value("Feedback Request", {
"is_sent": 1,
"is_feedback_submitted": 0,
"reference_doctype": "ToDo",
"reference_name": todo,
"feedback_trigger": feedback_trigger
}, ["name", "key"])
class TestFeedbackTrigger(unittest.TestCase):
def setUp(self):
new_user = frappe.get_doc(dict(doctype='User', email='test-feedback@example.com',
first_name='Tester')).insert(ignore_permissions=True)
new_user.add_roles("System Manager")
def tearDown(self):
frappe.db.sql("delete from tabContact where email_id='test-feedback@example.com'")
frappe.delete_doc("User", "test-feedback@example.com")
frappe.delete_doc("Feedback Trigger", "ToDo")
frappe.db.sql('delete from `tabEmail Queue`')
frappe.db.sql('delete from `tabFeedback Request`')
def test_feedback_trigger(self):
""" Test feedback trigger """
from frappe.www.feedback import accept
frappe.delete_doc("Feedback Trigger", "ToDo")
frappe.db.sql('delete from `tabEmail Queue`')
frappe.db.sql('delete from `tabFeedback Request`')
feedback_trigger = frappe.get_doc({
"enabled": 1,
"doctype": "Feedback Trigger",
"document_type": "ToDo",
"email_field": "assigned_by",
"email_fieldname": "assigned_by",
"subject": "{{ doc.name }} Task Completed",
"condition": "doc.status == 'Closed'",
"message": """Task {{ doc.name }} is Completed by {{ doc.owner }}.
regarding the Task {{ doc.name }}"""
}).insert(ignore_permissions=True)
# create a todo
todo = frappe.get_doc({
"doctype": "ToDo",
"owner": "test-feedback@example.com",
"assigned_by": "test-feedback@example.com",
"description": "Unable To Submit Sales Order #SO-00001"
}).insert(ignore_permissions=True)
# feedback alert mail should be sent only on 'Closed' status
email_queue = frappe.db.sql("""select name from `tabEmail Queue` where
reference_doctype='ToDo' and reference_name='{0}'""".format(todo.name))
self.assertFalse(email_queue)
# add a communication
frappe.get_doc({
"reference_doctype": "ToDo",
"reference_name": todo.name,
"communication_type": "Communication",
"content": "Test Communication",
"subject": "Test Communication",
"doctype": "Communication"
}).insert(ignore_permissions=True)
# check if feedback mail alert is triggered
todo.reload()
todo.status = "Closed"
todo.save(ignore_permissions=True)
email_queue = frappe.db.sql("""select name from `tabEmail Queue` where
reference_doctype='ToDo' and reference_name='{0}'""".format(todo.name))
self.assertTrue(email_queue)
# test if feedback is submitted for the todo
feedback_request, request_key = get_feedback_request(todo.name, feedback_trigger.name)
self.assertTrue(feedback_request)
# test if mail alerts are triggered multiple times for same document
todo.save(ignore_permissions=True)
email_queue = frappe.db.sql("""select name from `tabEmail Queue` where
reference_doctype='ToDo' and reference_name='{0}'""".format(todo.name))
self.assertTrue(len(email_queue) == 1)
frappe.db.sql('delete from `tabEmail Queue`')
# Test if feedback is submitted sucessfully
result = accept(request_key, "test-feedback@example.com", "ToDo", todo.name, "Great Work !!", 4, fullname="Test User")
self.assertTrue(result)
# test if feedback is saved in Communication
docname = frappe.db.get_value("Communication", {
"reference_doctype": "ToDo",
"reference_name": todo.name,
"communication_type": "Feedback",
"feedback_request": feedback_request
})
communication = frappe.get_doc("Communication", docname)
self.assertEqual(communication.rating, 4)
self.assertEqual(communication.content, "Great Work !!")
# test if link expired after feedback submission
self.assertRaises(Exception, accept, key=request_key, sender="test-feedback@example.com",
reference_doctype="ToDo", reference_name=todo.name, feedback="Thank You !!", rating=4, fullname="Test User")
# auto feedback request should trigger only once
todo.reload()
todo.save(ignore_permissions=True)
email_queue = frappe.db.sql("""select name from `tabEmail Queue` where
reference_doctype='ToDo' and reference_name='{0}'""".format(todo.name))
self.assertFalse(email_queue)
frappe.delete_doc("ToDo", todo.name)
# test if feedback requests and feedback communications are deleted?
communications = frappe.get_all("Communication", {
"reference_doctype": "ToDo",
"reference_name": todo.name,
"communication_type": "Feedback"
})
self.assertFalse(communications)
feedback_requests = frappe.get_all("Feedback Request", {
"reference_doctype": "ToDo",
"reference_name": todo.name,
"is_feedback_submitted": 0
})
self.assertFalse(feedback_requests)

View file

@ -290,6 +290,8 @@ class File(NestedSet):
zip_path = frappe.get_site_path(self.file_url.strip('/'))
base_url = os.path.dirname(self.file_url)
files = []
with zipfile.ZipFile(zip_path) as zf:
zf.extractall(os.path.dirname(zip_path))
for info in zf.infolist():
@ -308,8 +310,10 @@ class File(NestedSet):
file_doc.attached_to_doctype = self.attached_to_doctype
file_doc.attached_to_name = self.attached_to_name
file_doc.save()
files.append(file_doc)
frappe.delete_doc('File', self.name)
return files
def get_file_url(self):
@ -888,7 +892,8 @@ def get_random_filename(extn=None, content_type=None):
def unzip_file(name):
'''Unzip the given file and make file records for each of the extracted files'''
file_obj = frappe.get_doc('File', name)
file_obj.unzip()
files = file_obj.unzip()
return len(files)
@frappe.whitelist()
@ -919,3 +924,10 @@ def validate_filename(filename):
timestamp = now_datetime().strftime(" %Y-%m-%d %H:%M:%S")
fname = get_file_name(filename, timestamp)
return fname
@frappe.whitelist()
def get_files_in_folder(folder):
return frappe.db.get_all('File',
{ 'folder': folder },
['name', 'file_name', 'file_url', 'is_folder', 'modified']
)

View file

@ -3,9 +3,14 @@
frappe.ui.form.on('Page', {
refresh: function(frm) {
if(!frappe.boot.developer_mode && user != 'Administrator') {
if (!frappe.boot.developer_mode && frappe.session.user != 'Administrator') {
// make the document read-only
frm.set_read_only();
}
if (!frm.is_new() && !frm.doc.istable) {
frm.add_custom_button(__('Go to {0} Page', [frm.doc.title || frm.doc.name]), () => {
frappe.set_route(frm.doc.name);
});
}
}
});

View file

@ -21,6 +21,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "report_name",
"fieldtype": "Data",
"hidden": 1,
@ -53,6 +54,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "ref_report_doctype",
"fieldtype": "Link",
"hidden": 1,
@ -86,6 +88,8 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Queued",
"fetch_if_empty": 0,
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
@ -93,12 +97,12 @@
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Status",
"length": 0,
"no_copy": 0,
"options": "Queued\nCompleted",
"options": "Error\nQueued\nCompleted",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@ -119,6 +123,39 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_4",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "report_start_time",
"fieldtype": "Datetime",
"hidden": 0,
@ -151,6 +188,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "report_end_time",
"fieldtype": "Datetime",
"hidden": 0,
@ -183,6 +221,73 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.status == 'Error'",
"fetch_if_empty": 0,
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "error_message",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Error Message",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "filters_sb",
"fieldtype": "Section Break",
"hidden": 0,
@ -215,6 +320,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "filters",
"fieldtype": "Small Text",
"hidden": 1,
@ -247,6 +353,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "filter_values",
"fieldtype": "HTML",
"hidden": 0,
@ -279,6 +386,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "columns",
"fieldtype": "Code",
"hidden": 1,
@ -315,7 +423,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-10-23 16:58:14.879417",
"modified": "2019-04-19 12:39:47.211516",
"modified_by": "Administrator",
"module": "Core",
"name": "Prepared Report",

View file

@ -25,25 +25,43 @@ class PreparedReport(Document):
self.status = "Queued"
self.report_start_time = frappe.utils.now()
def after_insert(self):
def enqueue_report(self):
enqueue(
run_background,
instance=self, timeout=6000
prepared_report=self.name, timeout=6000
)
def on_trash(self):
remove_all("PreparedReport", self.name, from_delete=True)
def run_background(instance):
def run_background(prepared_report):
instance = frappe.get_doc("Prepared Report", prepared_report)
report = frappe.get_doc("Report", instance.ref_report_doctype)
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"])
instance.report_end_time = frappe.utils.now()
instance.save()
try:
report.custom_columns = []
if report.report_type == 'Custom Report':
custom_report_doc = report
reference_report = custom_report_doc.reference_report
report = frappe.get_doc("Report", reference_report)
report.custom_columns = custom_report_doc.json
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"])
instance.report_end_time = frappe.utils.now()
instance.save(ignore_permissions=True)
except Exception:
frappe.log_error(frappe.get_traceback())
instance = frappe.get_doc("Prepared Report", prepared_report)
instance.status = "Error"
instance.error_message = frappe.get_traceback()
instance.save(ignore_permissions=True)
frappe.publish_realtime(
'report_generated',

View file

@ -0,0 +1,12 @@
frappe.listview_settings['Prepared Report'] = {
add_fields: ["status"],
get_indicator: function(doc) {
if(doc.status==="Completed"){
return [__("Completed"), "green", "status,=,Completed"];
} else if(doc.status ==="Error"){
return [__("Error"), "red", "status,=,Error"];
} else if(doc.status ==="Queued"){
return [__("Queued"), "orange", "status,=,Queued"];
}
}
};

View file

@ -6,4 +6,4 @@ frappe.query_reports["{name}"] = {{
"filters": [
]
}}
}};

View file

@ -2,7 +2,7 @@
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
# import frappe
def execute(filters=None):
columns, data = [], []

View file

@ -100,7 +100,7 @@ class Report(Document):
columns = []
out = []
if self.report_type in ('Query Report', 'Script Report'):
if self.report_type in ('Query Report', 'Script Report', 'Custom Report'):
# query and script reports
data = frappe.desk.query_report.run(self.name, filters=filters, user=user)
for d in data.get('columns'):

View file

@ -12,6 +12,8 @@ from frappe.utils.user import get_system_managers
import frappe.permissions
import frappe.share
import re
import json
from frappe.limits import get_limits
from frappe.website.utils import is_signup_enabled
from frappe.utils.background_jobs import enqueue
@ -1086,4 +1088,12 @@ def generate_keys(user):
user_details.save()
return {"api_secret": api_secret}
frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)
frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)
@frappe.whitelist()
def update_profile_info(profile_info):
profile_info = json.loads(profile_info)
user = frappe.get_doc('User', frappe.session.user)
user.update(profile_info)
user.save()
return user

View file

@ -16,4 +16,4 @@ frappe.listview_settings['User'] = {
}
};
frappe.help.youtube_id["User"] = "fnBoRhBrwR4";
frappe.help.youtube_id["User"] = "8Slw1hsTmUI";

View file

@ -8,17 +8,26 @@ import frappe
import unittest
class TestUserPermission(unittest.TestCase):
def test_default_user_permission_validation(self):
user = create_user('test_default_permission@example.com')
param = get_params(user, 'User', user.name, is_default=1)
add_user_permissions(param)
#create a duplicate entry with default
perm_user = create_user('test_user_perm@example.com')
param = get_params(user, 'User', perm_user.name, is_default=1)
self.assertRaises(frappe.ValidationError, add_user_permissions, param)
def test_apply_to_all(self):
''' Create User permission for User having access to all applicable Doctypes'''
user = get_user()
param = get_params(user, apply = 1)
user = create_user('test_bulk_creation_update@example.com')
param = get_params(user, 'User', user.name)
created = add_user_permissions(param)
self.assertEquals(created, 1)
def test_for_applicable_on_update_from_apply_to_all(self):
''' Update User Permission from all to some applicable Doctypes'''
user = get_user()
param = get_params(user, applicable = ["Chat Room", "Chat Message"])
user = create_user('test_bulk_creation_update@example.com')
param = get_params(user, 'User', user.name , applicable = ["Chat Room", "Chat Message"])
create = add_user_permissions(param)
frappe.db.commit()
@ -33,8 +42,8 @@ class TestUserPermission(unittest.TestCase):
def test_for_apply_to_all_on_update_from_applicable(self):
''' Update User Permission from some to all applicable Doctypes'''
user = get_user()
param = get_params(user, apply = 1)
user = create_user('test_bulk_creation_update@example.com')
param = get_params(user, 'User', user.name)
created = add_user_permissions(param)
created_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user))
removed_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room"))
@ -46,26 +55,27 @@ class TestUserPermission(unittest.TestCase):
self.assertIsNone(removed_applicable_second)
self.assertEquals(created, 1)
def get_user():
if frappe.db.exists('User', 'test_bulk_creation_update@example.com'):
return frappe.get_doc('User', 'test_bulk_creation_update@example.com')
def create_user(email):
''' create user with role system manager '''
if frappe.db.exists('User', email):
return frappe.get_doc('User', email)
else:
user = frappe.new_doc('User')
user.email = 'test_bulk_creation_update@example.com'
user.first_name = 'Test_Bulk_Creation'
user.email = email
user.first_name = email.split("@")[0]
user.add_roles("System Manager")
return user
def get_params(user, apply = None , applicable = None):
def get_params(user, doctype, docname, is_default=0, applicable=None):
''' Return param to insert '''
param = {
"user": user.name,
"doctype":"User",
"docname":user.name
"doctype":doctype,
"docname":docname,
"is_default": is_default,
"apply_to_all_doctypes": 1,
"applicable_doctypes": []
}
if apply:
param.update({"apply_to_all_doctypes": 1})
param.update({"applicable_doctypes": []})
if applicable:
param.update({"apply_to_all_doctypes": 0})
param.update({"applicable_doctypes": applicable})

View file

@ -20,6 +20,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
@ -53,6 +54,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "allow",
"fieldtype": "Link",
"hidden": 0,
@ -86,6 +88,39 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "for_value",
"fieldtype": "Dynamic Link",
"hidden": 0,
@ -119,6 +154,40 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "is_default",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Default",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "advanced_control_section",
"fieldtype": "Section Break",
"hidden": 0,
@ -152,6 +221,7 @@
"collapsible": 0,
"columns": 0,
"default": "1",
"fetch_if_empty": 0,
"fieldname": "apply_to_all_doctypes",
"fieldtype": "Check",
"hidden": 0,
@ -185,6 +255,7 @@
"collapsible": 0,
"columns": 0,
"depends_on": "eval:!doc.apply_to_all_doctypes",
"fetch_if_empty": 0,
"fieldname": "applicable_for",
"fieldtype": "Link",
"hidden": 0,
@ -213,16 +284,14 @@
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-02-13 22:58:27.428741",
"modified": "2019-04-16 19:17:23.644724",
"modified_by": "Administrator",
"module": "Core",
"name": "User Permission",
@ -251,7 +320,6 @@
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",

View file

@ -7,21 +7,14 @@ import frappe, json
from frappe.model.document import Document
from frappe.permissions import (get_valid_perms, update_permission_property)
from frappe import _
from frappe.utils import cstr
from frappe.core.utils import find
from frappe.desk.form.linked_with import get_linked_doctypes
class UserPermission(Document):
def validate(self):
duplicate_exists = frappe.db.get_all(self.doctype, filters={
'allow': self.allow,
'for_value': self.for_value,
'user': self.user,
'applicable_for': self.applicable_for,
'apply_to_all_doctypes': self.apply_to_all_doctypes,
'name': ['!=', self.name]
}, limit=1)
if duplicate_exists:
frappe.throw(_("User permission already exists"), frappe.DuplicateEntryError)
self.validate_user_permission()
self.validate_default_permission()
def on_update(self):
frappe.cache().delete_value('user_permissions')
@ -31,6 +24,37 @@ class UserPermission(Document):
frappe.cache().delete_value('user_permissions')
frappe.publish_realtime('update_user_permissions')
def validate_user_permission(self):
''' checks for duplicate user permission records'''
duplicate_exists = frappe.db.get_all(self.doctype, filters={
'allow': self.allow,
'for_value': self.for_value,
'user': self.user,
'applicable_for': cstr(self.applicable_for),
'apply_to_all_doctypes': self.apply_to_all_doctypes,
'name': ['!=', self.name]
}, limit=1)
if duplicate_exists:
frappe.throw(_("User permission already exists"), frappe.DuplicateEntryError)
def validate_default_permission(self):
''' validate user permission overlap for default value of a particular doctype '''
overlap_exists = []
if self.is_default:
overlap_exists = frappe.get_all(self.doctype, filters={
'allow': self.allow,
'user': self.user,
'is_default': 1,
'name': ['!=', self.name]
}, or_filters={
'applicable_for': cstr(self.applicable_for),
'apply_to_all_doctypes': 1
}, limit=1)
if overlap_exists:
ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name)
frappe.throw(_("{0} has already assigned default value for {1}.".format(ref_link, self.allow)))
@frappe.whitelist()
def get_user_permissions(user=None):
'''Get all users permissions for the user as a dict of doctype'''
@ -52,7 +76,7 @@ def get_user_permissions(user=None):
out = {}
def add_doc_to_perm(perm, doc_name):
def add_doc_to_perm(perm, doc_name, is_default):
# group rules for each type
# for example if allow is "Customer", then build all allowed customers
# in a list
@ -61,21 +85,22 @@ def get_user_permissions(user=None):
out[perm.allow].append(frappe._dict({
'doc': doc_name,
'applicable_for': perm.get('applicable_for')
'applicable_for': perm.get('applicable_for'),
'is_default': is_default
}))
try:
for perm in frappe.get_all('User Permission',
fields=['allow', 'for_value', 'applicable_for'],
fields=['allow', 'for_value', 'applicable_for', 'is_default'],
filters=dict(user=user)):
meta = frappe.get_meta(perm.allow)
add_doc_to_perm(perm, perm.for_value)
add_doc_to_perm(perm, perm.for_value, perm.is_default)
if meta.is_nested_set():
decendants = frappe.db.get_descendants(perm.allow, perm.for_value)
for doc in decendants:
add_doc_to_perm(perm, doc)
add_doc_to_perm(perm, doc, False)
out = frappe._dict(out)
frappe.cache().hset("user_permissions", user, out)
@ -160,24 +185,25 @@ def add_user_permissions(data):
exists = frappe.db.exists("User Permission", {"user": data.user, "allow": data.doctype, "for_value": data.docname, "apply_to_all_doctypes": 1})
if data.apply_to_all_doctypes == 1 and not exists:
remove_applicable(d, data.user, data.doctype, data.docname)
insert_user_perm(data.user, data.doctype, data.docname, apply_to_all = 1)
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, apply_to_all = 1)
return 1
else:
remove_apply_to_all(data.user, data.doctype, data.docname)
update_applicable(d, data.applicable_doctypes, data.user, data.doctype, data.docname)
for applicable in data.applicable_doctypes :
if applicable not in d:
insert_user_perm(data.user, data.doctype, data.docname, applicable = applicable)
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, applicable = applicable)
elif exists:
insert_user_perm(data.user, data.doctype, data.docname, applicable = applicable)
insert_user_perm(data.user, data.doctype, data.docname, data.is_default, applicable = applicable)
return 1
return 0
def insert_user_perm(user, doctype, docname, apply_to_all=None, applicable=None):
def insert_user_perm(user, doctype, docname, is_default=0, apply_to_all=None, applicable=None):
user_perm = frappe.new_doc("User Permission")
user_perm.user = user
user_perm.allow = doctype
user_perm.for_value = docname
user_perm.is_default = is_default
if applicable:
user_perm.applicable_for = applicable
user_perm.apply_to_all_doctypes = 0
@ -210,4 +236,4 @@ def update_applicable(already_applied, to_apply, user, doctype, docname):
AND `applicable_for`=%s
AND `allow`=%s
AND `for_value`=%s
""",(user, applied, doctype, docname))
""",(user, applied, doctype, docname))

View file

@ -16,6 +16,7 @@ frappe.listview_settings['User Permission'] = {
dialog.fields_dict.doctype.set_input(undefined);
dialog.fields_dict.docname.set_input(undefined);
dialog.set_df_property("docname", "hidden", 1);
dialog.set_df_property("is_default", "hidden", 1);
dialog.set_df_property("apply_to_all_doctypes", "hidden", 1);
dialog.set_df_property("applicable_doctypes", "hidden", 1);
}
@ -53,11 +54,16 @@ frappe.listview_settings['User Permission'] = {
}
}
},
{
fieldname: 'is_default',
label: __('Is Default'),
fieldtype: 'Check',
hidden: 1
},
{
fieldname: 'apply_to_all_doctypes',
label: __('Apply to all Documents Types'),
fieldtype: 'Check',
checked: 1,
hidden: 1,
onchange: function() {
if(dialog.fields_dict.doctype.value && dialog.fields_dict.docname.value && dialog.fields_dict.user.value){
@ -205,8 +211,9 @@ frappe.listview_settings['User Permission'] = {
on_doctype_change: function(dialog) {
dialog.set_df_property("docname", "hidden", 0);
dialog.set_df_property("docname", "reqd", 1);
dialog.set_df_property("is_default", "hidden", 0);
dialog.set_df_property("apply_to_all_doctypes", "hidden", 0);
dialog.set_value("apply_to_all_doctypes","checked",1);
dialog.set_value("apply_to_all_doctypes", "checked", 1);
},
on_docname_change: function(dialog, options, applicable) {

View file

@ -40,6 +40,9 @@ def get_diff(old, new, for_child=False):
],
}'''
if not new:
return None
out = frappe._dict(changed = [], added = [], removed = [], row_changed = [])
for df in new.meta.fields:
if df.fieldtype in no_value_fields and df.fieldtype not in table_fields:

View file

@ -60,7 +60,12 @@ class Dashboard {
show_dashboard(current_dashboard_name) {
if(this.dashboard_name !== current_dashboard_name) {
this.dashboard_name = current_dashboard_name;
this.page.set_title(this.dashboard_name);
let title = this.dashboard_name;
if (!this.dashboard_name.toLowerCase().includes(__('dashboard'))) {
// ensure dashboard title has "dashboard"
title = __('{0} Dashboard', [title]);
}
this.page.set_title(title);
this.set_dropdown();
this.container.empty();
this.refresh();

View file

@ -1,60 +0,0 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.query_reports["Feedback Ratings"] = {
"filters": [
{
"fieldname": "document_type",
"label": __("Document Type"),
"fieldtype": "Link",
"options": "DocType",
"reqd": 1,
"default": "Issue",
"get_query": function() {
return {
"query": "frappe.core.report.feedback_ratings.feedback_ratings.get_document_type"
}
}
},
{
"fieldname": "document_id",
"label": __("Document ID"),
"fieldtype": "Dynamic Link",
"get_options": function() {
var document_type = frappe.query_report.get_filter_value('document_type');
if(!document_type) {
frappe.throw(__("Please select Document Type first"));
}
return document_type;
}
},
{
"fieldname":"from_date",
"label": __("From Date"),
"fieldtype": "Date",
'reqd': 1,
"default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30)
},
{
"fieldname":"to_date",
"label": __("To Date"),
"fieldtype": "Date",
'reqd': 1,
"default":frappe.datetime.nowdate()
}
],
get_chart_data: function(columns, result) {
return {
data: {
x: 'Date',
columns: [
['Date'].concat($.map(result, function(d) { return d[0]; })),
['Average Feedback'].concat($.map(result, function(d) { return d[1]; }))
]
},
chart_type: 'line',
}
}
}

View file

@ -1,23 +0,0 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2017-02-05 20:38:21.890174",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2017-02-24 19:56:51.141147",
"modified_by": "Administrator",
"module": "Core",
"name": "Feedback Ratings",
"owner": "Administrator",
"ref_doctype": "Feedback Trigger",
"report_name": "Feedback Ratings",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
}
]
}

View file

@ -1,58 +0,0 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
def execute(filters=None):
columns, data = get_columns(filters), get_data(filters)
return columns, data
def get_columns(filters):
return [
"Date:Date",
"Average Rating",
]
def get_data(filters):
data = []
document_type = filters.get("document_type")
party = filters.get("document_id")
filters = {
"reference_doctype": document_type,
"communication_type": "Feedback",
"creation": ["Between", [filters.get("from_date"), filters.get("to_date")]]
}
fields = ["DATE_FORMAT(DATE(creation),'%m-%d-%Y')", "avg(rating) as rating"]
if not document_type:
return []
if party:
filters.update({ "reference_name": party })
party_details = frappe.get_list("Communication", filters=filters, fields=fields,
order_by="creation", group_by="DATE_FORMAT(DATE(creation),'%m-%d-%Y')", as_list=True)
return party_details or []
@frappe.whitelist()
def get_document_type(doctype, txt, searchfield, start, page_len, filters):
""" get the document type """
document_type = []
txt = "%%%s%%" % txt
document_type = frappe.get_all("Feedback Trigger", filters={ "enabled": 1, "document_type": ("like", txt) },
fields=["document_type"], as_list=True)
document_type = map(list, document_type)
to_ignore = [ doc[0] for doc in document_type ]
documents = frappe.get_all("Feedback Request", filters={ "reference_doctype": ["not in", to_ignore] },
fields=["reference_doctype"], distinct=True, as_list=True)
if documents:
document_type.extend(documents)
return document_type

View file

@ -28,6 +28,11 @@ frappe.ui.form.on("Customize Form", {
$(frm.wrapper).on("grid-make-sortable", function(e, frm) {
frm.trigger("setup_sortable");
});
$(frm.wrapper).on("grid-move-row", function(e, frm) {
frm.trigger("setup_sortable");
});
},
doc_type: function(frm) {
@ -57,12 +62,13 @@ frappe.ui.form.on("Customize Form", {
frm.doc.fields.forEach(function(f, i) {
var data_row = frm.page.body.find('[data-fieldname="fields"] [data-idx="'+ f.idx +'"] .data-row');
if(!f.is_custom_field) {
data_row.removeClass('sortable-handle');
} else {
if(f.is_custom_field) {
data_row.addClass("highlight");
} else {
f._sortable = false;
}
});
frm.fields_dict.fields.grid.refresh();
},
refresh: function(frm) {

View file

@ -1,694 +1,182 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "DL.####",
"beta": 0,
"creation": "2013-01-29 17:55:08",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"autoname": "DL.####",
"creation": "2013-01-29 17:55:08",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"field_order": [
"doc_type",
"properties",
"label",
"default_print_format",
"max_attachments",
"allow_copy",
"istable",
"editable_grid",
"quick_entry",
"track_changes",
"track_views",
"image_view",
"column_break_5",
"title_field",
"image_field",
"search_fields",
"section_break_8",
"sort_field",
"column_break_10",
"sort_order",
"fields_section_break",
"fields"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "doc_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Enter Form Type",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "doc_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Enter Form Type",
"options": "DocType"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "doc_type",
"fieldname": "properties",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"depends_on": "doc_type",
"fieldname": "properties",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "label",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Change Label (via Custom Translation)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "label",
"fieldtype": "Data",
"label": "Change Label (via Custom Translation)"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "default_print_format",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Default Print Format",
"length": 0,
"no_copy": 0,
"options": "Print Format",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "default_print_format",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Default Print Format",
"options": "Print Format"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "max_attachments",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Max Attachments",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "max_attachments",
"fieldtype": "Int",
"label": "Max Attachments"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "allow_copy",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Hide Copy",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "allow_copy",
"fieldtype": "Check",
"label": "Hide Copy"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "istable",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Table",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "istable",
"fieldtype": "Check",
"label": "Is Table",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "istable",
"fieldname": "editable_grid",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Editable Grid",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"depends_on": "istable",
"fieldname": "editable_grid",
"fieldtype": "Check",
"label": "Editable Grid"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "quick_entry",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Quick Entry",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"default": "1",
"fieldname": "quick_entry",
"fieldtype": "Check",
"label": "Quick Entry"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "track_changes",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Track Changes",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "track_changes",
"fieldtype": "Check",
"label": "Track Changes"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval: doc.image_field",
"fieldname": "image_view",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Image View",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"depends_on": "eval: doc.image_field",
"fieldname": "image_view",
"fieldtype": "Check",
"label": "Image View"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_5",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Use this fieldname to generate title",
"fieldname": "title_field",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Title Field",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"description": "Use this fieldname to generate title",
"fieldname": "title_field",
"fieldtype": "Data",
"label": "Title Field"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Must be of type \"Attach Image\"",
"fieldname": "image_field",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Image Field",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"description": "Must be of type \"Attach Image\"",
"fieldname": "image_field",
"fieldtype": "Data",
"label": "Image Field"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Fields separated by comma (,) will be included in the \"Search By\" list of Search dialog box",
"fieldname": "search_fields",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Search Fields",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"description": "Fields separated by comma (,) will be included in the \"Search By\" list of Search dialog box",
"fieldname": "search_fields",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Search Fields"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "doc_type",
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"depends_on": "doc_type",
"fieldname": "section_break_8",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sort_field",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sort Field",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "sort_field",
"fieldtype": "Select",
"label": "Sort Field"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_10",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sort_order",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sort Order",
"length": 0,
"no_copy": 0,
"options": "ASC\nDESC",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "sort_order",
"fieldtype": "Select",
"label": "Sort Order",
"options": "ASC\nDESC"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "doc_type",
"description": "Customize Label, Print Hide, Default etc.",
"fieldname": "fields_section_break",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Fields",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"depends_on": "doc_type",
"description": "Customize Label, Print Hide, Default etc.",
"fieldname": "fields_section_break",
"fieldtype": "Section Break",
"label": "Fields"
},
{
"allow_bulk_edit": 1,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "fields",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Fields",
"length": 0,
"no_copy": 0,
"options": "Customize Form Field",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"allow_bulk_edit": 1,
"fieldname": "fields",
"fieldtype": "Table",
"label": "Fields",
"options": "Customize Form Field"
},
{
"fieldname": "track_views",
"fieldtype": "Check",
"label": "Track Views"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 1,
"icon": "fa fa-glass",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2017-04-21 16:59:12.752428",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",
"owner": "Administrator",
],
"hide_toolbar": 1,
"icon": "fa fa-glass",
"idx": 1,
"issingle": 1,
"modified": "2019-05-13 18:54:40.610862",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 0,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "doc_type",
"show_name_in_global_search": 0,
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
],
"quick_entry": 1,
"search_fields": "doc_type",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -28,6 +28,7 @@ doctype_properties = {
'editable_grid': 'Check',
'max_attachments': 'Int',
'track_changes': 'Check',
'track_views': 'Check',
}
docfield_properties = {
@ -87,6 +88,9 @@ class CustomizeForm(Document):
if self.doc_type in core_doctypes_list:
return frappe.msgprint(_("Core DocTypes cannot be customized."))
if meta.issingle:
return frappe.msgprint(_("Single DocTypes cannot be customized."))
if meta.custom:
return frappe.msgprint(_("Only standard DocTypes are allowed to be customized from Customize Form."))
@ -157,7 +161,7 @@ class CustomizeForm(Document):
frappe.db.updatedb(self.doc_type)
if not hasattr(self, 'hide_success') or not self.hide_success:
frappe.msgprint(_("{0} updated").format(_(self.doc_type)))
frappe.msgprint(_("{0} updated").format(_(self.doc_type)), alert=True)
frappe.clear_cache(doctype=self.doc_type)
self.fetch_to_customize()

View file

@ -23,14 +23,14 @@ def drop_user_and_database(db_name, root_login=None, root_password=None):
import frappe.database.mariadb.setup_db
return frappe.database.mariadb.setup_db.drop_user_and_database(db_name, root_login, root_password)
def get_db(host=None, user=None, password=None):
def get_db(host=None, user=None, password=None, port=None):
import frappe
if frappe.conf.db_type == 'postgres':
import frappe.database.postgres.database
return frappe.database.postgres.database.PostgresDatabase(host, user, password)
return frappe.database.postgres.database.PostgresDatabase(host, user, password, port=port)
else:
import frappe.database.mariadb.database
return frappe.database.mariadb.database.MariaDBDatabase(host, user, password)
return frappe.database.mariadb.database.MariaDBDatabase(host, user, password, port=port)
def setup_help_database(help_db_name):
import frappe
@ -39,4 +39,4 @@ def setup_help_database(help_db_name):
return frappe.database.postgres.setup_db.setup_help_database(help_db_name)
else:
import frappe.database.mariadb.setup_db
return frappe.database.mariadb.setup_db.setup_help_database(help_db_name)
return frappe.database.mariadb.setup_db.setup_help_database(help_db_name)

View file

@ -46,9 +46,10 @@ class Database(object):
class InvalidColumnName(frappe.ValidationError): pass
def __init__(self, host=None, user=None, password=None, ac_name=None, use_default=0):
def __init__(self, host=None, user=None, password=None, ac_name=None, use_default=0, port=None):
self.setup_type_map()
self.host = host or frappe.conf.db_host or 'localhost'
self.port = port or frappe.conf.db_port or ''
self.user = user or frappe.conf.db_name
self.db_name = frappe.conf.db_name
self._conn = None
@ -153,6 +154,10 @@ class Database(object):
frappe.log(values)
frappe.log(">>>>")
self._cursor.execute(query, values)
if frappe.flags.in_migrate:
self.log_touched_tables(query, values)
else:
if debug:
if explain:
@ -165,6 +170,9 @@ class Database(object):
self._cursor.execute(query)
if frappe.flags.in_migrate:
self.log_touched_tables(query)
if debug:
time_end = time()
frappe.errprint(("Execution time: {0} sec").format(round(time_end - time_start, 2)))
@ -173,6 +181,10 @@ class Database(object):
if(frappe.conf.db_type == 'postgres'):
self.rollback()
if frappe.conf.db_type == 'mariadb' and self.is_syntax_error(e):
frappe.errprint('Syntax error in query:')
frappe.errprint(query)
if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)):
pass
else:
@ -833,7 +845,7 @@ class Database(object):
"""Returns list of column names from given doctype."""
columns = self.get_db_table_columns('tab' + doctype)
if not columns:
raise self.ProgrammingError
raise self.TableMissingError
return columns
def has_column(self, doctype, column):
@ -912,6 +924,20 @@ class Database(object):
else:
frappe.throw('No conditions provided')
def log_touched_tables(self, query, values=None):
if values:
query = frappe.safe_decode(self._cursor.mogrify(query, values))
if query.strip().lower().split()[0] in ('insert', 'delete', 'update', 'alter'):
# ([`\"']?) Captures ', " or ` at the begining of the table name (if provided)
# (tab([A-Z]\w+)( [A-Z]\w+)*) Captures table names that start with "tab"
# and are continued with multiple words that start with a captital letter
# e.g. 'tabXxx' or 'tabXxx Xxx' or 'tabXxx Xxx Xxx' and so on
# \1 matches the first captured group (quote character) at the end of the table name
tables = [groups[1] for groups in re.findall(r'([`"\']?)(tab([A-Z]\w+)( [A-Z]\w+)*)\1', query)]
if frappe.flags.touched_tables is None:
frappe.flags.touched_tables = set()
frappe.flags.touched_tables.update(tables)
def enqueue_jobs_after_commit():
if frappe.flags.enqueue_after_commit and len(frappe.flags.enqueue_after_commit) > 0:

View file

@ -17,6 +17,7 @@ from frappe.database.mariadb.schema import MariaDBTable
class MariaDBDatabase(Database):
ProgrammingError = pymysql.err.ProgrammingError
TableMissingError = pymysql.err.ProgrammingError
OperationalError = pymysql.err.OperationalError
InternalError = pymysql.err.InternalError
SQLError = pymysql.err.ProgrammingError
@ -80,11 +81,11 @@ class MariaDBDatabase(Database):
if usessl:
conn = pymysql.connect(self.host, self.user or '', self.password or '',
charset='utf8mb4', use_unicode = True, ssl=ssl_params,
port=self.port, charset='utf8mb4', use_unicode = True, ssl=ssl_params,
conv = conversions, local_infile = frappe.conf.local_infile)
else:
conn = pymysql.connect(self.host, self.user or '', self.password or '',
charset='utf8mb4', use_unicode = True, conv = conversions,
port=self.port, charset='utf8mb4', use_unicode = True, conv = conversions,
local_infile = frappe.conf.local_infile)
# MYSQL_OPTION_MULTI_STATEMENTS_OFF = 1
@ -163,6 +164,10 @@ class MariaDBDatabase(Database):
def cant_drop_field_or_key(e):
return e.args[0] == ER.CANT_DROP_FIELD_OR_KEY
@staticmethod
def is_syntax_error(e):
return e.args[0] == ER.PARSE_ERROR
def is_primary_key_violation(self, e):
return self.is_duplicate_entry(e) and 'PRIMARY' in cstr(e.args[1])
@ -282,4 +287,4 @@ class MariaDBDatabase(Database):
self.begin()
def get_database_list(self, target):
return [d[0] for d in self.sql("SHOW DATABASES;")]
return [d[0] for d in self.sql("SHOW DATABASES;")]

View file

@ -37,6 +37,7 @@ CREATE TABLE `tabDocField` (
`unique` int(1) NOT NULL DEFAULT 0,
`no_copy` int(1) NOT NULL DEFAULT 0,
`allow_on_submit` int(1) NOT NULL DEFAULT 0,
`show_preview_popup` int(1) NOT NULL DEFAULT 0,
`trigger` varchar(255) DEFAULT NULL,
`collapsible_depends_on` text,
`depends_on` text,
@ -49,6 +50,7 @@ CREATE TABLE `tabDocField` (
`description` text,
`in_list_view` int(1) NOT NULL DEFAULT 0,
`in_standard_filter` int(1) NOT NULL DEFAULT 0,
`in_preview` int(1) NOT NULL DEFAULT 0,
`read_only` int(1) NOT NULL DEFAULT 0,
`precision` varchar(255) DEFAULT NULL,
`length` int(11) NOT NULL DEFAULT 0,

View file

@ -4,6 +4,32 @@ import frappe
import os, sys
from frappe.database.db_manager import DbManager
expected_settings_10_2_earlier = {
"innodb_file_format": "Barracuda",
"innodb_file_per_table": "ON",
"innodb_large_prefix": "ON",
"character_set_server": "utf8mb4",
"collation_server": "utf8mb4_unicode_ci"
}
expected_settings_10_3_later = {
"character_set_server": "utf8mb4",
"collation_server": "utf8mb4_unicode_ci"
}
def get_mariadb_versions():
# MariaDB classifies their versions as Major (1st and 2nd number), and Minor (3rd number)
# Example: Version 10.3.13 is Major Version = 10.3, Minor Version = 13
mariadb_variables = frappe._dict(frappe.db.sql("""show variables"""))
version_string = mariadb_variables.get('version').split('-')[0]
versions = {}
versions['major'] = version_string.split(
'.')[0] + '.' + version_string.split('.')[1]
versions['minor'] = version_string.split('.')[2]
return versions
def setup_database(force, source_sql, verbose):
frappe.local.session = frappe._dict({'user':'Administrator'})
@ -54,7 +80,10 @@ def drop_user_and_database(db_name, root_login, root_password):
def bootstrap_database(db_name, verbose, source_sql=None):
frappe.connect(db_name=db_name)
check_if_ready_for_barracuda()
if not check_database_settings():
print('Database settings do not match expected values; stopping database setup.')
sys.exit(1)
import_db_from_sql(source_sql, verbose)
if not 'tabDefaultValue' in frappe.db.get_tables():
print('''Database not installed, this can due to lack of permission, or that the database name exists.
@ -69,38 +98,33 @@ def import_db_from_sql(source_sql=None, verbose=False):
DbManager(frappe.local.db).restore_database(db_name, source_sql, db_name, frappe.conf.db_password)
if verbose: print("Imported from database %s" % source_sql)
def check_if_ready_for_barracuda():
def check_database_settings():
versions = get_mariadb_versions()
if versions['major'] <= '10.2':
expected_variables = expected_settings_10_2_earlier
else:
expected_variables = expected_settings_10_3_later
mariadb_variables = frappe._dict(frappe.db.sql("""show variables"""))
mariadb_minor_version = int(mariadb_variables.get('version').split('-')[0].split('.')[1])
if mariadb_minor_version < 3:
check_database(mariadb_variables, {
"innodb_file_format": "Barracuda",
"innodb_file_per_table": "ON",
"innodb_large_prefix": "ON"
})
check_database(mariadb_variables, {
"character_set_server": "utf8mb4",
"collation_server": "utf8mb4_unicode_ci"
})
# Check each expected value vs. actuals:
result = True
for key, expected_value in expected_variables.items():
if mariadb_variables.get(key) != expected_value:
print("For key %s. Expected value %s, found value %s" %
(key, expected_value, mariadb_variables.get(key)))
result = False
if not result:
site = frappe.local.site
msg = ("Creation of your site - {x} failed because MariaDB is not properly {sep}"
"configured. If using version 10.2.x or earlier, make sure you use the {sep}"
"the Barracuda storage engine. {sep}{sep}"
"Please verify the settings above in MariaDB's my.cnf. Restart MariaDB. And {sep}"
"then run `bench new-site {x}` again.{sep2}"
"").format(x=site, sep2="\n"*2, sep="\n")
print_db_config(msg)
return result
def check_database(mariadb_variables, variables_dict):
mariadb_minor_version = int(mariadb_variables.get('version').split('-')[0].split('.')[1])
for key, value in variables_dict.items():
if mariadb_variables.get(key) != value:
site = frappe.local.site
msg = ("Creation of your site - {x} failed because MariaDB is not properly {sep}"
"configured to use the Barracuda storage engine. {sep}"
"Please add the settings below to MariaDB's my.cnf, restart MariaDB then {sep}"
"run `bench new-site {x}` again.{sep2}"
"").format(x=site, sep2="\n"*2, sep="\n")
if mariadb_minor_version < 3:
print_db_config(msg, expected_config_for_barracuda_2)
else:
print_db_config(msg, expected_config_for_barracuda_3)
raise frappe.exceptions.ImproperDBConfigurationError(
reason="MariaDB default file format is not Barracuda"
)
def get_root_connection(root_login, root_password):
import getpass
@ -118,31 +142,8 @@ def get_root_connection(root_login, root_password):
return frappe.local.flags.root_connection
def print_db_config(explanation, config_text):
def print_db_config(explanation):
print("="*80)
print(explanation)
print(config_text)
print("="*80)
expected_config_for_barracuda_2 = """
[mysqld]
innodb-file-format=barracuda
innodb-file-per-table=1
innodb-large-prefix=1
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[mysql]
default-character-set = utf8mb4
"""
expected_config_for_barracuda_3 = """
[mysqld]
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[mysql]
default-character-set = utf8mb4
"""

View file

@ -21,6 +21,7 @@ psycopg2.extensions.register_type(DEC2FLOAT)
class PostgresDatabase(Database):
ProgrammingError = psycopg2.ProgrammingError
TableMissingError = psycopg2.ProgrammingError
OperationalError = psycopg2.OperationalError
InternalError = psycopg2.InternalError
SQLError = psycopg2.ProgrammingError
@ -63,10 +64,10 @@ class PostgresDatabase(Database):
def get_connection(self):
# warnings.filterwarnings('ignore', category=psycopg2.Warning)
conn = psycopg2.connect('host={} dbname={}'.format(self.host, self.user))
conn = psycopg2.connect('host={} dbname={} user={} password={} port={}'.format(
self.host, self.user, self.user, self.password, self.port
))
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) # TODO: Remove this
# conn = psycopg2.connect('host={} dbname={} user={} password={}'.format(self.host,
# self.user, self.user, self.password))
return conn
@ -309,4 +310,4 @@ def replace_locate_with_strpos(query):
# strpos is the locate equivalent in postgres
if re.search(r'locate\(', query, flags=re.IGNORECASE):
query = re.sub(r'locate\(([^,]+),([^)]+)\)', r'strpos(\2, \1)', query, flags=re.IGNORECASE)
return query
return query

View file

@ -37,6 +37,7 @@ CREATE TABLE "tabDocField" (
"unique" smallint NOT NULL DEFAULT 0,
"no_copy" smallint NOT NULL DEFAULT 0,
"allow_on_submit" smallint NOT NULL DEFAULT 0,
"show_preview_popup" smallint NOT NULL DEFAULT 0,
"trigger" varchar(255) DEFAULT NULL,
"collapsible_depends_on" text,
"depends_on" text,
@ -49,6 +50,7 @@ CREATE TABLE "tabDocField" (
"description" text,
"in_list_view" smallint NOT NULL DEFAULT 0,
"in_standard_filter" smallint NOT NULL DEFAULT 0,
"in_preview" smallint NOT NULL DEFAULT 0,
"read_only" smallint NOT NULL DEFAULT 0,
"precision" varchar(255) DEFAULT NULL,
"length" bigint NOT NULL DEFAULT 0,

View file

@ -40,7 +40,20 @@ class PostgresTable(DBTable):
query.append("ADD COLUMN `{}` {}".format(col.fieldname, col.get_definition()))
for col in self.change_type:
query.append("ALTER COLUMN `{}` TYPE {}".format(col.fieldname, get_definition(col.fieldtype, precision=col.precision, length=col.length)))
using_clause = ""
if col.fieldtype in ("Datetime"):
# The USING option of SET DATA TYPE can actually specify any expression
# involving the old values of the row
# read more https://www.postgresql.org/docs/9.1/sql-altertable.html
using_clause = "USING {}::timestamp without time zone".format(col.fieldname)
elif col.fieldtype in ("Check"):
using_clause = "USING {}::smallint".format(col.fieldname)
query.append("ALTER COLUMN {0} TYPE {1} {2}".format(
col.fieldname,
get_definition(col.fieldtype, precision=col.precision, length=col.length),
using_clause)
)
for col in self.set_default:
if col.fieldname=="name":
@ -93,4 +106,4 @@ class PostgresTable(DBTable):
fieldname, self.table_name)))
raise e
else:
raise e
raise e

View file

@ -1,4 +1,5 @@
import frappe, subprocess, os
from six.moves import input
def setup_database(force, source_sql, verbose):
root_conn = get_root_connection()
@ -10,9 +11,16 @@ def setup_database(force, source_sql, verbose):
frappe.conf.db_password))
root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(frappe.conf.db_name))
# we can't pass psql password in arguments in postgresql as mysql. So
# set password connection parameter in environment variable
subprocess_env = os.environ.copy()
subprocess_env['PGPASSWORD'] = str(frappe.conf.db_password)
# bootstrap db
subprocess.check_output(['psql', frappe.conf.db_name, '-qf',
os.path.join(os.path.dirname(__file__), 'framework_postgres.sql')])
subprocess.check_output([
'psql', frappe.conf.db_name, '-h', 'localhost', '-U',
frappe.conf.db_name, '-f',
os.path.join(os.path.dirname(__file__), 'framework_postgres.sql')
], env=subprocess_env)
frappe.connect()
@ -24,17 +32,20 @@ def setup_help_database(help_db_name):
root_conn.sql("CREATE user {0} password '{1}'".format(help_db_name, help_db_name))
root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(help_db_name))
def get_root_connection(root_login='postgres', root_password=None):
def get_root_connection(root_login=None, root_password=None):
import getpass
if not frappe.local.flags.root_connection:
if not root_login:
root_login = 'root'
root_login = frappe.conf.get("root_login") or None
if not root_login:
root_login = input("Enter postgres super user: ")
if not root_password:
root_password = frappe.conf.get("root_password") or None
if not root_password:
root_password = getpass.getpass("Postgres root password: ")
root_password = getpass.getpass("Postgres super user password: ")
frappe.local.flags.root_connection = frappe.database.get_db(user=root_login, password=root_password)

View file

@ -10,7 +10,7 @@ frappe.ui.form.on('Dashboard Chart', {
},
refresh: function(frm) {
frm.filters = null;
frm.chart_filters = null;
frm.set_df_property("filters_section", "hidden", 1);
frm.trigger('update_options');
},
@ -58,7 +58,7 @@ frappe.ui.form.on('Dashboard Chart', {
if (['Date', 'Datetime'].includes(df.fieldtype)) {
date_fields.push({label: df.label, value: df.fieldname});
}
if (['In', 'Float', 'Currency', 'Percent'].includes(df.fieldtype)) {
if (['Int', 'Float', 'Currency', 'Percent'].includes(df.fieldtype)) {
value_fields.push({label: df.label, value: df.fieldname});
}
});
@ -72,21 +72,26 @@ frappe.ui.form.on('Dashboard Chart', {
},
show_filters: function(frm) {
if (frm.filters) {
if (frm.chart_filters && frm.chart_filters.length) {
frm.trigger('render_filters_table');
} else {
if (frm.doc.chart_type==='Custom') {
frappe.xcall('frappe.desk.doctype.dashboard_chart_source.dashboard_chart_source.get_config', {name: frm.doc.source})
.then(config => {
frappe.dom.eval(config);
frm.filters = frappe.dashboards.chart_sources[frm.doc.source].filters;
frm.trigger('render_filters_table');
});
if (frm.doc.source) {
frappe.xcall('frappe.desk.doctype.dashboard_chart_source.dashboard_chart_source.get_config', {name: frm.doc.source})
.then(config => {
frappe.dom.eval(config);
frm.chart_filters = frappe.dashboards.chart_sources[frm.doc.source].filters;
frm.trigger('render_filters_table');
});
} else {
frm.chart_filters = [];
frm.trigger('render_filters_table');
}
} else {
// standard filters
if (frm.doc.document_type) {
// allow all link and select fields as filters
frm.filters = [];
frm.chart_filters = [];
frappe.model.with_doctype(frm.doc.document_type, () => {
frappe.get_meta(frm.doc.document_type).fields.map(df => {
if (['Link', 'Select'].includes(df.fieldtype)) {
@ -95,12 +100,11 @@ frappe.ui.form.on('Dashboard Chart', {
// nothing is mandatory
_df.reqd = 0;
_df.default = null;
_df.read_only = 0;
_df.permlevel = 1;
_df.hidden = 0;
// no default
if (!df.read_only && !df.hidden) {
frm.filters.push(_df);
}
frm.chart_filters.push(_df);
}
frm.trigger('render_filters_table');
});
@ -113,7 +117,7 @@ frappe.ui.form.on('Dashboard Chart', {
render_filters_table: function(frm) {
frm.set_df_property("filters_section", "hidden", 0);
let fields = frm.filters;
let fields = frm.chart_filters;
let wrapper = $(frm.get_field('filters_json').wrapper).empty();
let table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">

View file

@ -8,6 +8,10 @@ from frappe.utils import getdate
from frappe.desk.doctype.dashboard_chart.dashboard_chart import (get,
get_period_ending)
from datetime import datetime
from dateutil.relativedelta import relativedelta
import calendar
class TestDashboardChart(unittest.TestCase):
def test_period_ending(self):
self.assertEqual(get_period_ending('2019-04-10', 'Daily'),
@ -48,21 +52,13 @@ class TestDashboardChart(unittest.TestCase):
timeseries = 1
)).insert()
result = get(chart_name ='Test Dashboard Chart',
to_date = '2019-04-11', refresh = 1)
self.assertEqual(result.get('labels')[0], '2018-04-30')
self.assertEqual(result.get('labels')[1], '2018-05-31')
self.assertEqual(result.get('labels')[2], '2018-06-30')
self.assertEqual(result.get('labels')[3], '2018-07-31')
self.assertEqual(result.get('labels')[4], '2018-08-31')
self.assertEqual(result.get('labels')[5], '2018-09-30')
self.assertEqual(result.get('labels')[6], '2018-10-31')
self.assertEqual(result.get('labels')[7], '2018-11-30')
self.assertEqual(result.get('labels')[8], '2018-12-31')
self.assertEqual(result.get('labels')[9], '2019-01-31')
self.assertEqual(result.get('labels')[10], '2019-02-28')
self.assertEqual(result.get('labels')[11], '2019-03-31')
self.assertEqual(result.get('labels')[12], '2019-04-30')
cur_date = datetime.now() - relativedelta(years=1)
result = get(chart_name ='Test Dashboard Chart', refresh = 1)
for idx in range(13):
month = str(cur_date.year) + '-' + str(cur_date.strftime('%m')) + '-' + str(calendar.monthrange(cur_date.year, cur_date.month)[1])
self.assertEqual(result.get('labels')[idx], month)
cur_date += relativedelta(months=1)
# self.assertEqual(result.get('datasets')[0].get('values')[:-1],
# [44, 28, 8, 11, 2, 6, 18, 6, 4, 5, 15, 13])
@ -87,21 +83,13 @@ class TestDashboardChart(unittest.TestCase):
timeseries = 1
)).insert()
result = get(chart_name ='Test Empty Dashboard Chart',
to_date = '2019-04-11', refresh = 1)
self.assertEqual(result.get('labels')[0], '2018-04-30')
self.assertEqual(result.get('labels')[1], '2018-05-31')
self.assertEqual(result.get('labels')[2], '2018-06-30')
self.assertEqual(result.get('labels')[3], '2018-07-31')
self.assertEqual(result.get('labels')[4], '2018-08-31')
self.assertEqual(result.get('labels')[5], '2018-09-30')
self.assertEqual(result.get('labels')[6], '2018-10-31')
self.assertEqual(result.get('labels')[7], '2018-11-30')
self.assertEqual(result.get('labels')[8], '2018-12-31')
self.assertEqual(result.get('labels')[9], '2019-01-31')
self.assertEqual(result.get('labels')[10], '2019-02-28')
self.assertEqual(result.get('labels')[11], '2019-03-31')
self.assertEqual(result.get('labels')[12], '2019-04-30')
cur_date = datetime.now() - relativedelta(years=1)
result = get(chart_name ='Test Empty Dashboard Chart', refresh = 1)
for idx in range(13):
month = str(cur_date.year) + '-' + str(cur_date.strftime('%m')) + '-' + str(calendar.monthrange(cur_date.year, cur_date.month)[1])
self.assertEqual(result.get('labels')[idx], month)
cur_date += relativedelta(months=1)
frappe.db.rollback()
@ -126,24 +114,16 @@ class TestDashboardChart(unittest.TestCase):
timeseries = 1
)).insert()
result = get(chart_name ='Test Empty Dashboard Chart 2',
to_date = '2019-04-11', refresh = 1)
self.assertEqual(result.get('labels')[0], '2018-04-30')
self.assertEqual(result.get('labels')[1], '2018-05-31')
self.assertEqual(result.get('labels')[2], '2018-06-30')
self.assertEqual(result.get('labels')[3], '2018-07-31')
self.assertEqual(result.get('labels')[4], '2018-08-31')
self.assertEqual(result.get('labels')[5], '2018-09-30')
self.assertEqual(result.get('labels')[6], '2018-10-31')
self.assertEqual(result.get('labels')[7], '2018-11-30')
self.assertEqual(result.get('labels')[8], '2018-12-31')
self.assertEqual(result.get('labels')[9], '2019-01-31')
self.assertEqual(result.get('labels')[10], '2019-02-28')
self.assertEqual(result.get('labels')[11], '2019-03-31')
self.assertEqual(result.get('labels')[12], '2019-04-30')
cur_date = datetime.now() - relativedelta(years=1)
result = get(chart_name ='Test Empty Dashboard Chart 2', refresh = 1)
for idx in range(13):
month = str(cur_date.year) + '-' + str(cur_date.strftime('%m')) + '-' + str(calendar.monthrange(cur_date.year, cur_date.month)[1])
self.assertEqual(result.get('labels')[idx], month)
cur_date += relativedelta(months=1)
# only 1 data point with value
self.assertEqual(result.get('datasets')[0].get('values')[2], 1)
self.assertEqual(result.get('datasets')[0].get('values')[2], 0)
frappe.db.rollback()

View file

@ -43,10 +43,17 @@ class Event(Document):
def sync_communication(self):
if self.event_participants:
for participant in self.event_participants:
communication_name = frappe.db.get_value("Communication", dict(reference_doctype=self.doctype, reference_name=self.name, timeline_doctype=participant.reference_doctype, timeline_name=participant.reference_docname), "name")
if communication_name:
communication = frappe.get_doc("Communication", communication_name)
self.update_communication(participant, communication)
comms = frappe.get_list("Communication", filters=[
["Communication", "reference_doctype", "=", self.doctype],
["Communication", "reference_name", "=", self.name],
["Dynamic Link", "link_doctype", "=", participant.reference_doctype],
["Dynamic Link", "link_name", "=", participant.reference_docname]
], fields=["name"])
if comms:
for comm in comms:
communication = frappe.get_doc("Communication", comm.name)
self.update_communication(participant, communication)
else:
meta = frappe.get_meta(participant.reference_doctype)
if hasattr(meta, "allow_events_in_timeline") and meta.allow_events_in_timeline==1:
@ -62,12 +69,11 @@ class Event(Document):
communication.subject = self.subject
communication.content = self.description if self.description else self.subject
communication.communication_date = self.starts_on
communication.timeline_doctype = participant.reference_doctype
communication.timeline_name = participant.reference_docname
communication.reference_doctype = self.doctype
communication.reference_name = self.name
communication.communication_medium = communication_mapping[self.event_category] if self.event_category else ""
communication.status = "Linked"
communication.add_link(participant.reference_doctype, participant.reference_docname)
communication.save(ignore_permissions=True)
@frappe.whitelist()
@ -76,9 +82,18 @@ def delete_communication(event, reference_doctype, reference_docname):
if isinstance(event, string_types):
event = json.loads(event)
communication_name = frappe.db.get_value("Communication", dict(reference_doctype=event["doctype"], reference_name=event["name"], timeline_doctype=deleted_participant.reference_doctype, timeline_name=deleted_participant.reference_docname), "name")
if communication_name:
deletion = frappe.get_doc("Communication", communication_name).delete()
comms = frappe.get_list("Communication", filters=[
["Communication", "reference_doctype", "=", event.get("doctype")],
["Communication", "reference_name", "=", event.get("name")],
["Dynamic Link", "link_doctype", "=", deleted_participant.reference_doctype],
["Dynamic Link", "link_name", "=", deleted_participant.reference_docname]
], fields=["name"])
if comms:
deletion = []
for comm in comms:
delete = frappe.get_doc("Communication", comm.name).delete()
deletion.append(delete)
return deletion

View file

@ -21,6 +21,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "description_and_status",
"fieldtype": "Section Break",
"hidden": 0,
@ -53,6 +54,7 @@
"collapsible": 0,
"columns": 0,
"default": "Open",
"fetch_if_empty": 0,
"fieldname": "status",
"fieldtype": "Select",
"hidden": 0,
@ -86,6 +88,7 @@
"collapsible": 0,
"columns": 0,
"default": "Medium",
"fetch_if_empty": 0,
"fieldname": "priority",
"fieldtype": "Select",
"hidden": 0,
@ -120,6 +123,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
@ -150,6 +154,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "color",
"fieldtype": "Color",
"hidden": 0,
@ -157,7 +162,7 @@
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Color",
"length": 0,
@ -182,6 +187,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "date",
"fieldtype": "Date",
"hidden": 0,
@ -189,7 +195,7 @@
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "Due Date",
"length": 0,
@ -215,6 +221,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "owner",
"fieldtype": "Link",
"hidden": 0,
@ -247,6 +254,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "description_section",
"fieldtype": "Section Break",
"hidden": 0,
@ -279,6 +287,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "description",
"fieldtype": "Text Editor",
"hidden": 0,
@ -314,6 +323,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"hidden": 0,
@ -345,6 +355,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "reference_type",
"fieldtype": "Link",
"hidden": 0,
@ -352,7 +363,7 @@
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Reference Type",
"length": 0,
@ -379,6 +390,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"hidden": 0,
@ -413,6 +425,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_10",
"fieldtype": "Column Break",
"hidden": 0,
@ -443,6 +456,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "role",
"fieldtype": "Link",
"hidden": 0,
@ -477,6 +491,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "assigned_by",
"fieldtype": "Link",
"hidden": 0,
@ -510,6 +525,7 @@
"collapsible": 0,
"columns": 0,
"fetch_from": "assigned_by.full_name",
"fetch_if_empty": 0,
"fieldname": "assigned_by_full_name",
"fieldtype": "Read Only",
"hidden": 0,
@ -543,6 +559,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "sender",
"fieldtype": "Data",
"hidden": 1,
@ -575,6 +592,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "assignment_rule",
"fieldtype": "Link",
"hidden": 0,
@ -603,17 +621,16 @@
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-check",
"idx": 2,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-03-07 16:11:25.764549",
"menu_index": 0,
"modified": "2019-04-24 15:45:23.290491",
"modified_by": "Administrator",
"module": "Desk",
"name": "ToDo",
@ -660,7 +677,6 @@
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "description, reference_type, reference_name",
"show_name_in_global_search": 0,
"sort_order": "DESC",

View file

@ -43,8 +43,11 @@ class ToDo(Document):
def on_trash(self):
# unlink todo from linked comments
frappe.db.sql("""update `tabCommunication` set link_doctype=null, link_name=null
where link_doctype=%(doctype)s and link_name=%(name)s""", {"doctype": self.doctype, "name": self.name})
frappe.db.sql("""
delete from `tabDynamic Link`
where link_doctype=%(doctype)s and link_name=%(name)s""", {
"doctype": self.doctype, "name": self.name
})
self.update_in_reference()
@ -94,7 +97,7 @@ def get_permission_query_conditions(user):
if "System Manager" in frappe.get_roles(user):
return None
else:
return """(tabToDo.owner = {user} or tabToDo.assigned_by = {user})"""\
return """(`tabToDo`.owner = {user} or `tabToDo`.assigned_by = {user})"""\
.format(user=frappe.db.escape(user))
def has_permission(doc, user):
@ -108,4 +111,4 @@ def new_todo(description):
frappe.get_doc({
'doctype': 'ToDo',
'description': description
}).insert()
}).insert()

View file

@ -1,4 +1,7 @@
frappe.listview_settings['ToDo'] = {
hide_name_column: true,
add_fields: ["reference_type", "reference_name"],
onload: function(me) {
if (!frappe.route_options) {
frappe.route_options = {
@ -8,7 +11,22 @@ frappe.listview_settings['ToDo'] = {
}
me.page.set_title(__("To Do"));
},
hide_name_column: true,
button: {
show: function(doc) {
return doc.reference_name;
},
get_label: function() {
return __('Open');
},
get_description: function(doc) {
return __('Open {0}', [`${doc.reference_type} ${doc.reference_name}`])
},
action: function(doc) {
frappe.set_route('Form', doc.reference_type, doc.reference_name);
}
},
refresh: function(me) {
if (me.todo_sidebar_setup) return;
@ -19,5 +37,4 @@ frappe.listview_settings['ToDo'] = {
me.todo_sidebar_setup = true;
},
add_fields: ["reference_type", "reference_name"],
}

View file

@ -7,6 +7,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.desk.form.document_follow import follow_document
from frappe.utils import cint
import frappe.share
class DuplicateToDoError(frappe.ValidationError): pass
@ -177,7 +178,7 @@ def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE',
'notify': notify
}
if arg and arg.get("notify"):
if arg and cint(arg.get("notify")):
_notify(arg)
def _notify(args):

View file

@ -54,8 +54,8 @@ def unfollow_document(doctype, doc_name, user):
return 1
return 0
def get_message(doc_name, doctype, frequency):
activity_list = get_version(doctype, doc_name, frequency) + get_comments(doctype, doc_name, frequency)
def get_message(doc_name, doctype, frequency, user):
activity_list = get_version(doctype, doc_name, frequency, user) + get_comments(doctype, doc_name, frequency, user)
return sorted(activity_list, key=lambda k: k["time"], reverse=True)
def send_email_alert(receiver, docinfo, timeline):
@ -98,7 +98,7 @@ def send_document_follow_mails(frequency):
valid_document_follows = []
if user_frequency == frequency:
for d in grouped_by_user[user]:
content = get_message(d.ref_docname, d.ref_doctype, frequency)
content = get_message(d.ref_docname, d.ref_doctype, frequency, user)
if content:
message = message + content
valid_document_follows.append({
@ -107,13 +107,13 @@ def send_document_follow_mails(frequency):
"reference_url": get_url_to_form(d.ref_doctype, d.ref_docname)
})
if message:
if message and frappe.db.get_value("User", user, "document_follow_notify", ignore=True):
send_email_alert(user, valid_document_follows, message)
def get_version(doctype, doc_name, frequency):
def get_version(doctype, doc_name, frequency, user):
timeline = []
filters = get_filters("docname", doc_name, frequency)
filters = get_filters("docname", doc_name, frequency, user)
version = frappe.get_all("Version",
filters=filters,
fields=["ref_doctype", "data", "modified", "modified", "modified_by"]
@ -134,9 +134,9 @@ def get_version(doctype, doc_name, frequency):
return timeline
def get_comments(doctype, doc_name, frequency):
def get_comments(doctype, doc_name, frequency, user):
timeline = []
filters = get_filters("reference_name", doc_name, frequency)
filters = get_filters("reference_name", doc_name, frequency, user)
comments = frappe.get_all("Comment",
filters=filters,
fields=["content", "modified", "modified_by", "comment_type"]
@ -255,26 +255,29 @@ def send_daily_updates():
def send_weekly_updates():
send_document_follow_mails("Weekly")
def get_filters(search_by, name, frequency):
def get_filters(search_by, name, frequency, user):
filters = []
if frequency == "Weekly":
filters = [
[search_by, "=", name],
["modified", ">", frappe.utils.add_days(frappe.utils.nowdate(),-7)],
["modified", "<", frappe.utils.nowdate()]
["modified", "<", frappe.utils.nowdate()],
["modified_by", "!=", user]
]
elif frequency == "Daily":
filters = [
[search_by, "=", name],
["modified", ">", frappe.utils.add_days(frappe.utils.nowdate(),-1)],
["modified", "<", frappe.utils.nowdate()]
["modified", "<", frappe.utils.nowdate()],
["modified_by", "!=", user]
]
elif frequency == "Hourly":
filters = [
[search_by, "=", name],
["modified", ">", frappe.utils.add_to_date(frappe.utils.now_datetime(), 0, 0, 0, -1)],
["modified", "<", frappe.utils.now_datetime()]
["modified", ">", frappe.utils.add_to_date(frappe.utils.now_datetime(), hours=-1)],
["modified", "<", frappe.utils.now_datetime()],
["modified_by", "!=", user]
]
return filters

View file

@ -47,9 +47,6 @@ def getdoc(doctype, name, user=None):
frappe.errprint(frappe.utils.get_traceback())
raise
if doc and not name.startswith('_'):
frappe.get_user().update_recent(doctype, name)
doc.add_seen()
frappe.response.docs.append(doc)
@ -100,13 +97,16 @@ def get_docinfo(doc=None, doctype=None, name=None):
"assignments": get_assignments(doc.doctype, doc.name),
"permissions": get_doc_permissions(doc),
"shared": frappe.share.get_users(doc.doctype, doc.name),
"rating": get_feedback_rating(doc.doctype, doc.name),
"views": get_view_logs(doc.doctype, doc.name),
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
"is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user),
"document_follow_enabled": frappe.db.get_value("User", frappe.session.user, "document_follow_notify")
"milestones": get_milestones(doc.doctype, doc.name),
"is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user)
}
def get_milestones(doctype, name):
return frappe.db.get_all('Milestone', fields = ['creation', 'owner', 'track_field', 'value'],
filters=dict(reference_type=doctype, reference_name=name))
def get_attachments(dt, dn):
return frappe.get_all("File", fields=["name", "file_name", "file_url", "is_private"],
filters = {"attached_to_name": dn, "attached_to_doctype": dt})
@ -160,36 +160,50 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=
group_by=None, as_dict=True):
'''Returns list of communications for a given document'''
if not fields:
fields = '''`name`, `communication_type`,`communication_medium`, `comment_type`,
`communication_date`, `content`, `sender`, `sender_full_name`,
`creation`, `subject`, `delivery_status`, `_liked_by`,
`timeline_doctype`, `timeline_name`, `reference_doctype`, `reference_name`,
`link_doctype`, `link_name`, `read_by_recipient`, `rating`, 'Communication' AS `doctype`'''
fields = '''
`tabCommunication`.name, `tabCommunication`.communication_type, `tabCommunication`.communication_medium,
`tabCommunication`.comment_type, `tabCommunication`.communication_date, `tabCommunication`.content,
`tabCommunication`.sender, `tabCommunication`.sender_full_name, `tabCommunication`.cc, `tabCommunication`.bcc,
`tabCommunication`.creation, `tabCommunication`.subject, `tabCommunication`.delivery_status,
`tabCommunication`._liked_by, `tabCommunication`.reference_doctype, `tabCommunication`.reference_name,
`tabCommunication`.read_by_recipient, `tabCommunication`.rating
'''
conditions = '''communication_type in ('Communication', 'Feedback')
and (
(reference_doctype=%(doctype)s and reference_name=%(name)s)
conditions = '''
`tabCommunication`.communication_type in ('Communication', 'Feedback')
and (
(`tabCommunication`.reference_doctype=%(doctype)s and `tabCommunication`.reference_name=%(name)s)
or (
(timeline_doctype=%(doctype)s and timeline_name=%(name)s)
and (communication_type='Communication')
(`tabDynamic Link`.link_doctype=%(doctype)s and `tabDynamic Link`.link_name=%(name)s)
and (`tabCommunication`.communication_type='Communication')
)
)'''
)
'''
if after:
# find after a particular date
conditions+= ' and creation > {0}'.format(after)
conditions += '''
and `tabCommunication`.creation > {0}
'''.format(after)
if doctype=='User':
conditions+= " and not (reference_doctype='User' and communication_type='Communication')"
conditions += '''
and not (`tabCommunication`.reference_doctype='User' and `tabCommunication`.communication_type='Communication')
'''
communications = frappe.db.sql("""select {fields}
communications = frappe.db.sql('''
select distinct {fields}
from `tabCommunication`
inner join `tabDynamic Link`
on `tabCommunication`.name=`tabDynamic Link`.parent
where {conditions} {group_by}
order by creation desc LIMIT %(limit)s OFFSET %(start)s""".format(
fields = fields, conditions=conditions, group_by=group_by or ""),
{ "doctype": doctype, "name": name, "start": frappe.utils.cint(start), "limit": limit },
as_dict=as_dict)
order by `tabCommunication`.creation desc
limit %(limit)s offset %(start)s'''.format(fields = fields, conditions=conditions, group_by=group_by or ""),{
"doctype": doctype,
"name": name,
"start": frappe.utils.cint(start),
"limit": limit
}, as_dict=as_dict)
return communications
@ -218,21 +232,6 @@ def run_onload(doc):
doc.set("__onload", frappe._dict())
doc.run_method("onload")
def get_feedback_rating(doctype, docname):
""" get and return the latest feedback rating if available """
rating= frappe.get_all("Communication", filters={
"reference_doctype": doctype,
"reference_name": docname,
"communication_type": "Feedback"
}, fields=["rating"], order_by="creation desc", as_list=True)
if not rating:
return 0
else:
return rating[0][0]
def get_view_logs(doctype, docname):
""" get and return the latest view logs if available """
logs = []
@ -244,4 +243,4 @@ def get_view_logs(doctype, docname):
if view_logs:
logs = view_logs
return logs
return logs

View file

@ -27,11 +27,8 @@ def savedocs(doc, action):
# update recent documents
run_onload(doc)
frappe.get_user().update_recent(doc.doctype, doc.name)
send_updated_docs(doc)
except Exception:
if not frappe.local.message_log:
frappe.msgprint(frappe._('Did not save'))
frappe.errprint(frappe.utils.get_traceback())
raise

View file

@ -0,0 +1,28 @@
import frappe
from frappe.model import no_value_fields
import json
@frappe.whitelist()
def get_preview_data(doctype, docname, fields):
fields = json.loads(fields)
preview_fields = [field['name'] for field in fields if field['type'] not in no_value_fields]
preview_fields.append(frappe.get_meta(doctype).get_title_field())
if 'name' not in fields:
preview_fields.append('name')
preview_fields.append(frappe.get_meta(doctype).image_field)
preview_data = frappe.get_list(doctype, filters={
'name': docname
}, fields=preview_fields, limit=1)
if preview_data:
preview_data = preview_data[0]
preview_data = {k: v for k, v in preview_data.items() if v is not None}
for k,v in preview_data.items():
if frappe.get_meta(doctype).has_field(k):
preview_data[k] = frappe.format(v,frappe.get_meta(doctype).get_field(k).fieldtype)
if not preview_data:
return None
return preview_data

View file

@ -383,7 +383,7 @@ def get_report_list(module, is_standard="No"):
out.append({
"type": "report",
"doctype": r.ref_doctype,
"is_query_report": 1 if r.report_type in ("Query Report", "Script Report") else 0,
"is_query_report": 1 if r.report_type in ("Query Report", "Script Report", "Custom Report") else 0,
"label": _(r.name),
"name": r.name
})

View file

@ -38,7 +38,8 @@ def get_feed(start, page_length):
{match_conditions_comment}
) X
order by X.creation DESC
limit %(start)s, %(page_length)s"""
LIMIT %(page_length)s
OFFSET %(start)s"""
.format(match_conditions_comment = match_conditions_comment,
match_conditions_communication = match_conditions_communication), {
"user": frappe.session.user,
@ -55,4 +56,4 @@ def get_heatmap_data():
where
date(creation) > subdate(curdate(), interval 1 year)
group by date(creation)
order by creation asc"""))
order by creation asc"""))

View file

@ -389,7 +389,7 @@ def make_records(records, debug=False):
# pass DuplicateEntryError and continue
if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name:
# make sure DuplicateEntryError is for the exact same doc and not a related doc
pass
frappe.clear_messages()
else:
raise

View file

@ -132,6 +132,8 @@ def background_enqueue_run(report_name, filters=None, user=None):
})
track_instance.insert(ignore_permissions=True)
frappe.db.commit()
track_instance.enqueue_report()
return {
"name": track_instance.name,
"redirect_url": get_url_to_form("Prepared Report", track_instance.name)
@ -224,7 +226,7 @@ def add_data_to_custom_columns(columns, result):
fieldname = column['fieldname']
key = (column['doctype'], fieldname)
link_field = column['link_field']
row[fieldname] = custom_fields_data.get(key, {}).get(row[link_field])
row[fieldname] = custom_fields_data.get(key, {}).get(row.get(link_field))
return data
@ -280,6 +282,10 @@ def export_query():
filters = json.loads(data["filters"])
if isinstance(data.get("report_name"), string_types):
report_name = data["report_name"]
frappe.permissions.can_export(
frappe.get_cached_value('Report', report_name, 'ref_doctype'),
raise_exception=True
)
if isinstance(data.get("file_format_type"), string_types):
file_format_type = data["file_format_type"]

View file

@ -108,7 +108,7 @@ def save_report():
d.report_type = "Report Builder"
d.json = data['json']
frappe.get_doc(d).save()
frappe.msgprint(_("{0} is saved").format(d.name))
frappe.msgprint(_("{0} is saved").format(d.name), alert=True)
return d.name
@frappe.whitelist()

View file

@ -54,9 +54,9 @@ frappe.ui.form.on('Auto Email Report', {
show_filters: function(frm) {
var wrapper = $(frm.get_field('filters_display').wrapper);
wrapper.empty();
if(frm.doc.report_type !== 'Report Builder'
if(frm.doc.report_type === 'Custom Report' || (frm.doc.report_type !== 'Report Builder'
&& frappe.query_reports[frm.doc.report]
&& frappe.query_reports[frm.doc.report].filters) {
&& frappe.query_reports[frm.doc.report].filters)) {
// make a table to show filters
var table = $('<table class="table table-bordered" style="cursor:pointer; margin:0px;"><thead>\
@ -65,7 +65,17 @@ frappe.ui.form.on('Auto Email Report', {
$('<p class="text-muted small">' + __("Click table to edit") + '</p>').appendTo(wrapper);
var filters = JSON.parse(frm.doc.filters || '{}');
var report_filters = frappe.query_reports[frm.doc.report].filters;
let report_filters;
if (frm.doc.report_type === 'Custom Report'
&& frappe.query_reports[frm.doc.reference_report]
&& frappe.query_reports[frm.doc.reference_report].filters) {
report_filters = frappe.query_reports[frm.doc.reference_report].filters;
} else {
report_filters = frappe.query_reports[frm.doc.report].filters;
}
if(report_filters && report_filters.length > 0) {
frm.set_value('filter_meta', JSON.stringify(report_filters));
}
@ -99,6 +109,14 @@ frappe.ui.form.on('Auto Email Report', {
dialog.show();
dialog.set_values(filters);
})
// populate dynamic date field selection
let date_fields = report_filters
.filter(df => df.fieldtype === 'Date')
.map(df => ({ label: df.label, value: df.fieldname }));
frm.set_df_property('from_date_field', 'options', date_fields);
frm.set_df_property('to_date_field', 'options', date_fields);
frm.toggle_display('dynamic_report_filters_section', date_fields.length > 0);
}
}
});

Some files were not shown because too many files have changed in this diff Show more