Merge branch 'develop' of https://github.com/frappe/frappe into web-form-refactor

This commit is contained in:
Shivam Mishra 2019-05-23 19:42:39 +05:30
commit c85b76aeaa
199 changed files with 4413 additions and 12903 deletions

View file

@ -39,7 +39,7 @@ 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`;

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(host=local.conf.slave_host, user=local.conf.slave_db_name,
password=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
@ -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

@ -8,7 +8,7 @@ from frappe import _
import frappe
import frappe.database
import frappe.utils
from frappe.utils import cint, flt, get_datetime, datetime
from frappe.utils import cint, flt, get_datetime, datetime, date_diff, today
import frappe.utils.user
from frappe import conf
from frappe.sessions import Session, clear_sessions, delete_session
@ -124,6 +124,12 @@ class LoginManager:
frappe.clear_cache(user = frappe.form_dict.get('usr'))
user, pwd = get_cached_user_pass()
self.authenticate(user=user, pwd=pwd)
if self.force_user_to_reset_password():
doc = frappe.get_doc("User", self.user)
frappe.local.response["redirect_to"] = doc.reset_password(send_email=False, password_expired=True)
frappe.local.response["message"] = "Password Reset"
return False
if should_run_2fa(self.user):
authenticate_for_2factor(self.user)
if not confirm_otp_token(self):
@ -209,6 +215,22 @@ class LoginManager:
self.check_if_enabled(user)
self.user = self.check_password(user, pwd)
def force_user_to_reset_password(self):
if not self.user:
return
reset_pwd_after_days = cint(frappe.db.get_single_value("System Settings",
"force_user_to_reset_password"))
if reset_pwd_after_days:
last_password_reset_date = frappe.db.get_value("User",
self.user, "last_password_reset_date") or today()
last_pwd_reset_days = date_diff(today(), last_password_reset_date)
if last_pwd_reset_days > reset_pwd_after_days:
return True
def check_if_enabled(self, user):
"""raise exception if user not enabled"""
doc = frappe.get_doc("System Settings")

View file

@ -16,7 +16,6 @@ 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
@ -77,7 +76,6 @@ 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))

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

@ -72,12 +72,11 @@ def call_command(cmd, context):
def get_commands():
# prevent circular imports
from .docs import commands as doc_commands
from .scheduler import commands as scheduler_commands
from .site import commands as site_commands
from .translate import commands as translate_commands
from .utils import commands as utils_commands
return list(set(doc_commands + scheduler_commands + site_commands + translate_commands + utils_commands))
return list(set(scheduler_commands + site_commands + translate_commands + utils_commands))
commands = get_commands()

View file

@ -1,57 +0,0 @@
from __future__ import unicode_literals, absolute_import
import click
import os, shutil
import frappe
from frappe.commands import pass_context
@click.command('build-docs')
@pass_context
@click.argument('app')
@click.option('--docs-version', default='current')
@click.option('--target', default=None)
@click.option('--local', default=False, is_flag=True, help='Run app locally')
@click.option('--watch', default=False, is_flag=True, help='Watch for changes and rewrite')
def build_docs(context, app, docs_version="current", target=None, local=False, watch=False):
"Setup docs in target folder of target app"
from frappe.utils import watch as start_watch
from frappe.utils.setup_docs import add_breadcrumbs_tag
for site in context.sites:
_build_docs_once(site, app, docs_version, target, local)
if watch:
def trigger_make(source_path, event_type):
if "/docs/user/" in source_path:
# user file
target_path = frappe.get_app_path(target, 'www', 'docs', 'user',
os.path.relpath(source_path, start=frappe.get_app_path(app, 'docs', 'user')))
shutil.copy(source_path, target_path)
add_breadcrumbs_tag(target_path)
if source_path.endswith('/docs/index.md'):
target_path = frappe.get_app_path(target, 'www', 'docs', 'index.md')
shutil.copy(source_path, target_path)
apps_path = frappe.get_app_path(app)
start_watch(apps_path, handler=trigger_make)
def _build_docs_once(site, app, docs_version, target, local, only_content_updated=False):
from frappe.utils.setup_docs import setup_docs
try:
frappe.init(site=site)
frappe.connect()
make = setup_docs(app, target)
if not only_content_updated:
make.build(docs_version)
#make.make_docs(target, local)
finally:
frappe.destroy()
commands = [
build_docs,
]

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

@ -4,7 +4,7 @@
from __future__ import unicode_literals
from six import iteritems
import frappe
from frappe import _
field_map = {
"Contact": [ "first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact" ],
@ -94,6 +94,9 @@ def get_reference_details(reference_doctype, doctype, reference_list, reference_
for d in records:
temp_records.append(d[1:])
if not reference_list:
frappe.throw(_("No records present in {0}".format(reference_doctype)))
reference_details[reference_list[0]][frappe.scrub(doctype)] = temp_records
return reference_details

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,67 @@ 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"])
frappe.db.add_index("Communication Link", ["link_doctype", "link_name"])
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 +310,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,130 @@ 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):
frappe.delete_doc_if_exists("Note", "deduplication timeline links")
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
frappe.delete_doc_if_exists("Note", "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

@ -0,0 +1,47 @@
{
"creation": "2019-05-21 09:47:23.043960",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"link_doctype",
"link_name",
"link_title"
],
"fields": [
{
"fieldname": "link_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Link DocType",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "link_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Link Name",
"options": "link_doctype",
"reqd": 1
},
{
"fieldname": "link_title",
"fieldtype": "Read Only",
"in_list_view": 1,
"label": "Link Title",
"read_only": 1
}
],
"istable": 1,
"modified": "2019-05-21 09:47:23.043960",
"modified_by": "Administrator",
"module": "Core",
"name": "Communication Link",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- 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 CommunicationLink(Document):
pass

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,

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,14 @@ import frappe.website.render
import json
class InvalidFieldNameError(frappe.ValidationError): pass
class UniqueFieldnameError(frappe.ValidationError): pass
class IllegalMandatoryError(frappe.ValidationError): pass
class DoctypeLinkError(frappe.ValidationError): pass
class WrongOptionsDoctypeLinkError(frappe.ValidationError): pass
class HiddenAndMandatoryWithoutDefaultError(frappe.ValidationError): pass
class NonUniqueError(frappe.ValidationError): pass
class CannotIndexedError(frappe.ValidationError): pass
class CannotCreateStandardDoctypeError(frappe.ValidationError): pass
form_grid_templates = {
"fields": "templates/form_grid/fields.html"
@ -101,7 +109,7 @@ class DocType(Document):
return
if not frappe.conf.get("developer_mode") and not self.custom:
frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."))
frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."), CannotCreateStandardDoctypeError)
def setup_fields_to_fetch(self):
'''Setup query to update values for newly set fetch values'''
@ -542,7 +550,6 @@ def validate_fields_for_doctype(doctype):
# this is separate because it is also called via custom field
def validate_fields(meta):
"""Validate doctype fields. Checks
1. There are no illegal characters in fieldnames
2. If fieldnames are unique.
3. Validate column length.
@ -562,38 +569,38 @@ def validate_fields(meta):
def check_illegal_characters(fieldname):
validate_column_name(fieldname)
def check_unique_fieldname(fieldname):
def check_unique_fieldname(docname, fieldname):
duplicates = list(filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields)))
if len(duplicates) > 1:
frappe.throw(_("Fieldname {0} appears multiple times in rows {1}").format(fieldname, ", ".join(duplicates)))
frappe.throw(_("{0}: Fieldname {1} appears multiple times in rows {2}").format(docname, fieldname, ", ".join(duplicates)), UniqueFieldnameError)
def check_fieldname_length(fieldname):
validate_column_length(fieldname)
def check_illegal_mandatory(d):
def check_illegal_mandatory(docname, d):
if (d.fieldtype in no_value_fields) and d.fieldtype not in table_fields and d.reqd:
frappe.throw(_("Field {0} of type {1} cannot be mandatory").format(d.label, d.fieldtype))
frappe.throw(_("{0}: Field {1} of type {2} cannot be mandatory").format(docname, d.label, d.fieldtype), IllegalMandatoryError)
def check_link_table_options(d):
def check_link_table_options(docname, d):
if d.fieldtype in ("Link",) + table_fields:
if not d.options:
frappe.throw(_("Options required for Link or Table type field {0} in row {1}").format(d.label, d.idx))
frappe.throw(_("{0}: Options required for Link or Table type field {1} in row {2}").format(docname, d.label, d.idx), DoctypeLinkError)
if d.options=="[Select]" or d.options==d.parent:
return
if d.options != d.parent:
options = frappe.db.get_value("DocType", d.options, "name")
if not options:
frappe.throw(_("Options must be a valid DocType for field {0} in row {1}").format(d.label, d.idx))
frappe.throw(_("{0}: Options must be a valid DocType for field {1} in row {2}").format(docname, d.label, d.idx), WrongOptionsDoctypeLinkError)
elif not (options == d.options):
frappe.throw(_("Options {0} must be the same as doctype name {1} for the field {2}")
.format(d.options, options, d.label))
frappe.throw(_("{0}: Options {1} must be the same as doctype name {2} for the field {3}", DoctypeLinkError)
.format(docname, d.options, options, d.label))
else:
# fix case
d.options = options
def check_hidden_and_mandatory(d):
def check_hidden_and_mandatory(docname, d):
if d.hidden and d.reqd and not d.default:
frappe.throw(_("Field {0} in row {1} cannot be hidden and mandatory without default").format(d.label, d.idx))
frappe.throw(_("{0}: Field {1} in row {2} cannot be hidden and mandatory without default").format(docname, d.label, d.idx), HiddenAndMandatoryWithoutDefaultError)
def check_width(d):
if d.fieldtype == "Currency" and cint(d.width) < 100:
@ -616,7 +623,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))
@ -625,14 +634,14 @@ def validate_fields(meta):
if d.fieldtype in ("Currency", "Float", "Percent") and d.precision is not None and not (1 <= cint(d.precision) <= 6):
frappe.throw(_("Precision should be between 1 and 6"))
def check_unique_and_text(d):
def check_unique_and_text(docname, d):
if meta.issingle:
d.unique = 0
d.search_index = 0
if getattr(d, "unique", False):
if d.fieldtype not in ("Data", "Link", "Read Only"):
frappe.throw(_("Fieldtype {0} for {1} cannot be unique").format(d.fieldtype, d.label))
frappe.throw(_("{0}: Fieldtype {1} for {2} cannot be unique").format(docname, d.fieldtype, d.label), NonUniqueError)
if not d.get("__islocal") and frappe.db.has_column(d.parent, d.fieldname):
has_non_unique_values = frappe.db.sql("""select `{fieldname}`, count(*)
@ -641,10 +650,10 @@ def validate_fields(meta):
doctype=d.parent, fieldname=d.fieldname))
if has_non_unique_values and has_non_unique_values[0][0]:
frappe.throw(_("Field '{0}' cannot be set as Unique as it has non-unique values").format(d.label))
frappe.throw(_("{0}: Field '{1}' cannot be set as Unique as it has non-unique values").format(docname, d.label), NonUniqueError)
if d.search_index and d.fieldtype in ("Text", "Long Text", "Small Text", "Code", "Text Editor"):
frappe.throw(_("Fieldtype {0} for {1} cannot be indexed").format(d.fieldtype, d.label))
frappe.throw(_("{0}:Fieldtype {1} for {2} cannot be indexed").format(docname, d.fieldtype, d.label), CannotIndexedError)
def check_fold(fields):
fold_exists = False
@ -795,16 +804,16 @@ def validate_fields(meta):
d.fieldname = d.fieldname.lower()
check_illegal_characters(d.fieldname)
check_unique_fieldname(d.fieldname)
check_unique_fieldname(meta.get("name"), d.fieldname)
check_fieldname_length(d.fieldname)
check_illegal_mandatory(d)
check_link_table_options(d)
check_illegal_mandatory(meta.get("name"), d)
check_link_table_options(meta.get("name"), d)
check_dynamic_link_options(d)
check_hidden_and_mandatory(d)
check_hidden_and_mandatory(meta.get("name"), d)
check_in_list_view(d)
check_in_global_search(d)
check_illegal_default(d)
check_unique_and_text(d)
check_unique_and_text(meta.get("name"), d)
check_illegal_depends_on_conditions(d)
check_table_multiselect_option(d)
scrub_options_in_select(d)

View file

@ -5,6 +5,8 @@ from __future__ import unicode_literals
import frappe
import unittest
from frappe.core.doctype.doctype.doctype import UniqueFieldnameError, IllegalMandatoryError, DoctypeLinkError, WrongOptionsDoctypeLinkError,\
HiddenAndMandatoryWithoutDefaultError, CannotIndexedError
# test_records = frappe.get_test_records('DocType')
@ -226,3 +228,55 @@ class TestDocType(unittest.TestCase):
raise
finally:
frappe.flags.allow_doctype_export = 0
def test_unique_field_name_for_two_fields(self):
doc = self.new_doctype('Test Unique Field')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Data'
field_2 = doc.append('fields', {})
field_2.fieldname = 'some_fieldname_1'
field_2.fieldtype = 'Data'
self.assertRaises(UniqueFieldnameError, doc.insert)
def test_illegal_mandatory_validation(self):
doc = self.new_doctype('Test Illegal mandatory')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Section Break'
field_1.reqd = 1
self.assertRaises(IllegalMandatoryError, doc.insert)
def test_link_with_wrong_and_no_options(self):
doc = self.new_doctype('Test link')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Link'
self.assertRaises(DoctypeLinkError, doc.insert)
field_1.options = 'wrongdoctype'
self.assertRaises(WrongOptionsDoctypeLinkError, doc.insert)
def test_hidden_and_mandatory_without_default(self):
doc = self.new_doctype('Test hidden and mandatory')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Data'
field_1.reqd = 1
field_1.hidden = 1
self.assertRaises(HiddenAndMandatoryWithoutDefaultError, doc.insert)
def test_field_can_not_be_indexed_validation(self):
doc = self.new_doctype('Test index')
field_1 = doc.append('fields', {})
field_1.fieldname = 'some_fieldname_1'
field_1.fieldtype = 'Long Text'
field_1.search_index = 1
self.assertRaises(CannotIndexedError, doc.insert)

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

@ -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

@ -25,7 +25,7 @@ class PreparedReport(Document):
self.status = "Queued"
self.report_start_time = frappe.utils.now()
def after_insert(self):
def enqueue_report(self):
enqueue(
run_background,
prepared_report=self.name, timeout=6000
@ -54,14 +54,14 @@ def run_background(prepared_report):
instance.status = "Completed"
instance.columns = json.dumps(result["columns"])
instance.report_end_time = frappe.utils.now()
instance.save()
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()
instance.save(ignore_permissions=True)
frappe.publish_realtime(
'report_generated',

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

@ -1,5 +1,6 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
@ -18,6 +19,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "localization",
"fieldtype": "Section Break",
"hidden": 0,
@ -49,6 +51,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "country",
"fieldtype": "Link",
"hidden": 0,
@ -82,6 +85,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "language",
"fieldtype": "Link",
"hidden": 0,
@ -114,6 +118,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
@ -145,6 +150,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "time_zone",
"fieldtype": "Select",
"hidden": 0,
@ -176,6 +182,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "is_first_startup",
"fieldtype": "Check",
"hidden": 1,
@ -208,6 +215,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "setup_complete",
"fieldtype": "Check",
"hidden": 1,
@ -240,6 +248,7 @@
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "date_and_number_format",
"fieldtype": "Section Break",
"hidden": 0,
@ -271,6 +280,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "date_format",
"fieldtype": "Select",
"hidden": 0,
@ -303,6 +313,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_7",
"fieldtype": "Column Break",
"hidden": 0,
@ -334,6 +345,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "number_format",
"fieldtype": "Select",
"hidden": 0,
@ -366,6 +378,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "float_precision",
"fieldtype": "Select",
"hidden": 0,
@ -399,6 +412,7 @@
"collapsible": 0,
"columns": 0,
"description": "If not set, the currency precision will depend on number format",
"fetch_if_empty": 0,
"fieldname": "currency_precision",
"fieldtype": "Select",
"hidden": 0,
@ -432,6 +446,7 @@
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "sec_backup_limit",
"fieldtype": "Section Break",
"hidden": 0,
@ -466,6 +481,7 @@
"columns": 0,
"default": "3",
"description": "Older backups will be automatically deleted",
"fetch_if_empty": 0,
"fieldname": "backup_limit",
"fieldtype": "Int",
"hidden": 0,
@ -498,6 +514,7 @@
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "background_workers",
"fieldtype": "Section Break",
"hidden": 0,
@ -531,6 +548,7 @@
"collapsible": 0,
"columns": 0,
"description": "Run scheduled jobs only if checked",
"fetch_if_empty": 0,
"fieldname": "enable_scheduler",
"fieldtype": "Check",
"hidden": 1,
@ -562,6 +580,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "scheduler_last_event",
"fieldtype": "Data",
"hidden": 1,
@ -594,6 +613,7 @@
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "permissions",
"fieldtype": "Section Break",
"hidden": 0,
@ -628,6 +648,7 @@
"columns": 0,
"default": "0",
"description": "If Apply Strict User Permission is checked and User Permission is defined for a DocType for a User, then all the documents where value of the link is blank, will not be shown to that User",
"fetch_if_empty": 0,
"fieldname": "apply_strict_user_permissions",
"fieldtype": "Check",
"hidden": 0,
@ -660,6 +681,7 @@
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "security",
"fieldtype": "Section Break",
"hidden": 0,
@ -693,6 +715,7 @@
"columns": 0,
"default": "06:00",
"description": "Session Expiry in Hours e.g. 06:00",
"fetch_if_empty": 0,
"fieldname": "session_expiry",
"fieldtype": "Data",
"hidden": 0,
@ -727,6 +750,7 @@
"columns": 0,
"default": "720:00",
"description": "In Hours",
"fetch_if_empty": 0,
"fieldname": "session_expiry_mobile",
"fieldtype": "Data",
"hidden": 0,
@ -759,75 +783,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"description": "If enabled, the password strength will be enforced based on the Minimum Password Score value. A value of 2 being medium strong and 4 being very strong.",
"fieldname": "enable_password_policy",
"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": "Enable Password Policy",
"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,
"default": "2",
"depends_on": "eval:doc.enable_password_policy==1",
"fieldname": "minimum_password_score",
"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": "Minimum Password Score",
"length": 0,
"no_copy": 0,
"options": "2\n3\n4",
"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": "column_break_13",
"fieldtype": "Column Break",
"hidden": 0,
@ -860,6 +816,7 @@
"collapsible": 0,
"columns": 0,
"description": "Note: Multiple sessions will be allowed in case of mobile device",
"fetch_if_empty": 0,
"fieldname": "deny_multiple_sessions",
"fieldtype": "Check",
"hidden": 0,
@ -894,6 +851,7 @@
"columns": 0,
"default": "0",
"description": "User can login using Email id or Mobile number",
"fetch_if_empty": 0,
"fieldname": "allow_login_using_mobile_number",
"fieldtype": "Check",
"hidden": 0,
@ -928,6 +886,7 @@
"columns": 0,
"default": "0",
"description": "User can login using Email id or User Name",
"fetch_if_empty": 0,
"fieldname": "allow_login_using_user_name",
"fieldtype": "Check",
"hidden": 0,
@ -962,6 +921,7 @@
"columns": 0,
"default": "1",
"description": "",
"fetch_if_empty": 0,
"fieldname": "allow_error_traceback",
"fieldtype": "Check",
"hidden": 0,
@ -994,6 +954,177 @@
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "password_settings",
"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": "Password",
"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,
"description": "In Days",
"fetch_if_empty": 0,
"fieldname": "force_user_to_reset_password",
"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": "Force User to Reset Password",
"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": "column_break_31",
"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,
"default": "1",
"description": "If enabled, the password strength will be enforced based on the Minimum Password Score value. A value of 2 being medium strong and 4 being very strong.",
"fetch_if_empty": 0,
"fieldname": "enable_password_policy",
"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": "Enable Password Policy",
"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,
"default": "2",
"depends_on": "eval:doc.enable_password_policy==1",
"fetch_if_empty": 0,
"fieldname": "minimum_password_score",
"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": "Minimum Password Score",
"length": 0,
"no_copy": 0,
"options": "2\n3\n4",
"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": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "brute_force_security",
"fieldtype": "Section Break",
"hidden": 0,
@ -1026,6 +1157,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "allow_consecutive_login_attempts",
"fieldtype": "Int",
"hidden": 0,
@ -1058,6 +1190,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_34",
"fieldtype": "Column Break",
"hidden": 0,
@ -1091,6 +1224,7 @@
"columns": 0,
"default": "60",
"description": "In seconds",
"fetch_if_empty": 0,
"fieldname": "allow_login_after_fail",
"fieldtype": "Int",
"hidden": 0,
@ -1123,6 +1257,7 @@
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "two_factor_authentication",
"fieldtype": "Section Break",
"hidden": 0,
@ -1155,6 +1290,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "enable_two_factor_auth",
"fieldtype": "Check",
"hidden": 0,
@ -1190,6 +1326,7 @@
"default": "0",
"depends_on": "enable_two_factor_auth",
"description": "If enabled, users who login from Restricted IP Address, won't be prompted for Two Factor Auth",
"fetch_if_empty": 0,
"fieldname": "bypass_2fa_for_retricted_ip_users",
"fieldtype": "Check",
"hidden": 0,
@ -1224,6 +1361,7 @@
"columns": 0,
"depends_on": "enable_two_factor_auth",
"description": "If enabled, all users can login from any IP Address using Two Factor Auth. This can also be set only for specific user(s) in User Page",
"fetch_if_empty": 0,
"fieldname": "bypass_restrict_ip_check_if_2fa_enabled",
"fieldtype": "Check",
"hidden": 0,
@ -1259,6 +1397,7 @@
"default": "OTP App",
"depends_on": "",
"description": "Choose authentication method to be used by all users",
"fetch_if_empty": 0,
"fieldname": "two_factor_method",
"fieldtype": "Select",
"hidden": 0,
@ -1294,6 +1433,7 @@
"columns": 0,
"depends_on": "eval:doc.two_factor_method == \"OTP App\"",
"description": "Time in seconds to retain QR code image on server. Min:<strong>240</strong>",
"fetch_if_empty": 0,
"fieldname": "lifespan_qrcode_image",
"fieldtype": "Int",
"hidden": 0,
@ -1328,6 +1468,7 @@
"columns": 0,
"default": "Frappe Framework",
"depends_on": "enable_two_factor_auth",
"fetch_if_empty": 0,
"fieldname": "otp_issuer_name",
"fieldtype": "Data",
"hidden": 0,
@ -1361,6 +1502,7 @@
"bold": 0,
"collapsible": 1,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "email",
"fieldtype": "Section Break",
"hidden": 0,
@ -1394,6 +1536,7 @@
"collapsible": 0,
"columns": 0,
"description": "Your organization name and address for the email footer.",
"fetch_if_empty": 0,
"fieldname": "email_footer_address",
"fieldtype": "Small Text",
"hidden": 0,
@ -1426,6 +1569,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_18",
"fieldtype": "Column Break",
"hidden": 0,
@ -1457,6 +1601,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "disable_standard_email_footer",
"fieldtype": "Check",
"hidden": 0,
@ -1489,6 +1634,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "hide_footer_in_auto_email_reports",
"fieldtype": "Check",
"hidden": 0,
@ -1521,6 +1667,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "chat",
"fieldtype": "Section Break",
"hidden": 0,
@ -1554,6 +1701,7 @@
"collapsible": 0,
"columns": 0,
"default": "1",
"fetch_if_empty": 0,
"fieldname": "enable_chat",
"fieldtype": "Check",
"hidden": 0,
@ -1587,6 +1735,7 @@
"collapsible": 0,
"columns": 0,
"default": "1",
"fetch_if_empty": 0,
"fieldname": "use_socketio_to_upload_file",
"fieldtype": "Check",
"hidden": 0,
@ -1624,7 +1773,7 @@
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2019-01-30 11:02:41.011412",
"modified": "2019-04-16 13:26:09.247487",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

View file

@ -7,7 +7,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.model import no_value_fields
from frappe.translate import set_default_language
from frappe.utils import cint
from frappe.utils import cint, today
from frappe.utils.momentjs import get_all_timezones
from frappe.twofactor import toggle_two_factor_auth
@ -35,6 +35,11 @@ class SystemSettings(Document):
self.bypass_2fa_for_retricted_ip_users = 0
self.bypass_restrict_ip_check_if_2fa_enabled = 0
frappe.flags.update_last_reset_password_date = False
if (self.force_user_to_reset_password and
not cint(frappe.db.get_single_value("System Settings", "force_user_to_reset_password"))):
frappe.flags.update_last_reset_password_date = True
def on_update(self):
for df in self.meta.get("fields"):
if df.fieldtype not in no_value_fields:
@ -47,6 +52,16 @@ class SystemSettings(Document):
frappe.cache().delete_value('time_zone')
frappe.local.system_settings = {}
if frappe.flags.update_last_reset_password_date:
update_last_reset_password_date()
def update_last_reset_password_date():
frappe.db.sql(""" UPDATE `tabUser`
SET
last_password_reset_date = %s
WHERE
last_password_reset_date is null or last_password_reset_date = ''""", today())
@frappe.whitelist()
def load():
if not "System Manager" in frappe.get_roles():

View file

@ -1144,6 +1144,39 @@
"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": "last_password_reset_date",
"fieldtype": "Date",
"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": "Last Password Reset Date",
"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,
@ -2437,7 +2470,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 5,
"modified": "2019-03-03 11:10:06.162541",
"modified": "2019-04-15 20:25:02.022893",
"modified_by": "Administrator",
"module": "Core",
"name": "User",

View file

@ -4,7 +4,7 @@
from __future__ import unicode_literals, print_function
import frappe
from frappe.model.document import Document
from frappe.utils import cint, has_gravatar, format_datetime, now_datetime, get_formatted_email
from frappe.utils import cint, has_gravatar, format_datetime, now_datetime, get_formatted_email, today
from frappe import throw, msgprint, _
from frappe.utils.password import update_password as _update_password
from frappe.desk.notifications import clear_notifications
@ -218,13 +218,17 @@ class User(Document):
def validate_reset_password(self):
pass
def reset_password(self, send_email=False):
def reset_password(self, send_email=False, password_expired=False):
from frappe.utils import random_string, get_url
key = random_string(32)
self.db_set("reset_password_key", key)
link = get_url("/update-password?key=" + key)
url = "/update-password?key=" + key
if password_expired:
url = "/update-password?key=" + key + '&password_expired=true'
link = get_url(url)
if send_email:
self.password_reset_mail(link)
@ -591,6 +595,9 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password=
frappe.local.login_manager.login_as(user)
frappe.db.set_value("User", user,
'last_password_reset_date', today())
if user_doc.user_type == "System User":
return "/desk"
else:

View file

@ -53,7 +53,7 @@ class UserPermission(Document):
}, limit=1)
if overlap_exists:
ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name)
frappe.throw(_("{0} has already assigned default vaue for {1}.".format(ref_link, self.allow)))
frappe.throw(_("{0} has already assigned default value for {1}.".format(ref_link, self.allow)))
@frappe.whitelist()
def get_user_permissions(user=None):
@ -236,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

@ -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

@ -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."))

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

@ -64,10 +64,10 @@ class PostgresDatabase(Database):
def get_connection(self):
# warnings.filterwarnings('ignore', category=psycopg2.Warning)
conn = psycopg2.connect('host={} dbname={} port={}'.format(self.host, self.user, self.port))
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
@ -310,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

@ -100,6 +100,9 @@ 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;
frm.chart_filters.push(_df);
}

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],
["Communication Link", "link_doctype", "=", participant.reference_doctype],
["Communication 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")],
["Communication Link", "link_doctype", "=", deleted_participant.reference_doctype],
["Communication 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

@ -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 `tabCommunication 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

@ -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,7 +97,6 @@ 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),
"milestones": get_milestones(doc.doctype, doc.name),
@ -164,36 +160,59 @@ 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`, `cc`, `bcc`,
`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`'''
conditions = '''communication_type in ('Communication', 'Feedback')
and (
(reference_doctype=%(doctype)s and reference_name=%(name)s)
or (
(timeline_doctype=%(doctype)s and timeline_name=%(name)s)
and (communication_type='Communication')
)
)'''
fields = '''
C.name, C.communication_type, C.communication_medium,
C.comment_type, C.communication_date, C.content,
C.sender, C.sender_full_name, C.cc, C.bcc,
C.creation AS creation, C.subject, C.delivery_status,
C._liked_by, C.reference_doctype, C.reference_name,
C.read_by_recipient, C.rating
'''
conditions = ''
if after:
# find after a particular date
conditions+= ' and creation > {0}'.format(after)
conditions += '''
AND C.creation > {0}
'''.format(after)
if doctype=='User':
conditions+= " and not (reference_doctype='User' and communication_type='Communication')"
conditions += '''
AND NOT (C.reference_doctype='User' AND C.communication_type='Communication')
'''
communications = frappe.db.sql("""select {fields}
from `tabCommunication`
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)
# communications linked to reference_doctype
part1 = '''
SELECT {fields}
FROM `tabCommunication` as C
WHERE C.communication_type IN ('Communication', 'Feedback')
AND (C.reference_doctype = %(doctype)s AND C.reference_name = %(name)s)
{conditions}
'''.format(fields=fields, conditions=conditions)
# communications linked in Timeline Links
part2 = '''
SELECT {fields}
FROM `tabCommunication` as C
INNER JOIN `tabCommunication Link` ON C.name=`tabCommunication Link`.parent
WHERE C.communication_type IN ('Communication', 'Feedback')
AND `tabCommunication Link`.link_doctype = %(doctype)s AND `tabCommunication Link`.link_name = %(name)s
{conditions}
'''.format(fields=fields, conditions=conditions)
communications = frappe.db.sql('''
SELECT *
FROM (({part1}) UNION ({part2})) AS combined
{group_by}
ORDER BY creation DESC
LIMIT %(limit)s
OFFSET %(start)s
'''.format(part1=part1, part2=part2, group_by=(group_by or '')), dict(
doctype=doctype,
name=name,
start=frappe.utils.cint(start),
limit=limit
), as_dict=as_dict)
return communications
@ -222,21 +241,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 = []
@ -248,4 +252,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)
@ -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));
}

File diff suppressed because it is too large Load diff

View file

@ -387,7 +387,7 @@ class EmailAccount(Document):
communication._seen = json.dumps(users)
communication.flags.in_receive = True
communication.insert(ignore_permissions = 1)
communication.insert(ignore_permissions=True)
# save attachments
communication._attachments = email.save_attachments_in_doc(communication)
@ -470,7 +470,7 @@ class EmailAccount(Document):
parent = frappe.db.get_all(self.append_to, filters={
self.sender_field: email.from_email,
self.subject_field: ("like", "%{0}%".format(subject)),
"creation": (">", (get_datetime() - relativedelta(days=10)).strftime(DATE_FORMAT))
"creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT))
}, fields="name")
# match only subject field
@ -479,7 +479,7 @@ class EmailAccount(Document):
if not parent and len(subject) > 10 and is_system_user(email.from_email):
parent = frappe.db.get_all(self.append_to, filters={
self.subject_field: ("like", "%{0}%".format(subject)),
"creation": (">", (get_datetime() - relativedelta(days=10)).strftime(DATE_FORMAT))
"creation": (">", (get_datetime() - relativedelta(days=60)).strftime(DATE_FORMAT))
}, fields="name")
if parent:

View file

@ -26,7 +26,7 @@ class TestEmailAccount(unittest.TestCase):
email_account.db_set("enable_incoming", 0)
def test_incoming(self):
frappe.db.sql("delete from tabCommunication where sender='test_sender@example.com'")
cleanup("test_sender@example.com")
with open(os.path.join(os.path.dirname(__file__), "test_mails", "incoming-1.raw"), "r") as f:
test_mails = [f.read()]
@ -52,7 +52,8 @@ class TestEmailAccount(unittest.TestCase):
"reference_name": comm.reference_name, "status":"Not Sent"}))
def test_incoming_with_attach(self):
frappe.db.sql("DELETE FROM `tabCommunication` WHERE sender='test_sender@example.com'")
cleanup("test_sender@example.com")
existing_file = frappe.get_doc({'doctype': 'File', 'file_name': 'erpnext-conf-14.png'})
frappe.delete_doc("File", existing_file.name)
@ -75,7 +76,7 @@ class TestEmailAccount(unittest.TestCase):
def test_incoming_attached_email_from_outlook_plain_text_only(self):
frappe.db.sql("delete from tabCommunication where sender='test_sender@example.com'")
cleanup("test_sender@example.com")
with open(os.path.join(os.path.dirname(__file__), "test_mails", "incoming-3.raw"), "r") as f:
test_mails = [f.read()]
@ -88,7 +89,7 @@ class TestEmailAccount(unittest.TestCase):
self.assertTrue("This is an e-mail message sent automatically by Microsoft Outlook while" in comm.content)
def test_incoming_attached_email_from_outlook_layers(self):
frappe.db.sql("delete from tabCommunication where sender='test_sender@example.com'")
cleanup("test_sender@example.com")
with open(os.path.join(os.path.dirname(__file__), "test_mails", "incoming-4.raw"), "r") as f:
test_mails = [f.read()]
@ -123,8 +124,7 @@ class TestEmailAccount(unittest.TestCase):
self.assertTrue("test-mail-002" in sent_mail.get("Subject"))
def test_threading(self):
frappe.db.sql("""delete from tabCommunication
where sender in ('test_sender@example.com', 'test@example.com')""")
cleanup(["in", ['test_sender@example.com', 'test@example.com']])
# send
sent_name = make(subject = "Test", content="test content",
@ -149,8 +149,7 @@ class TestEmailAccount(unittest.TestCase):
self.assertEqual(comm.reference_name, sent.reference_name)
def test_threading_by_subject(self):
frappe.db.sql("""delete from tabCommunication
where sender in ('test_sender@example.com', 'test@example.com')""")
cleanup(["in", ['test_sender@example.com', 'test@example.com']])
with open(os.path.join(os.path.dirname(__file__), "test_mails", "reply-2.raw"), "r") as f:
test_mails = [f.read()]
@ -170,7 +169,7 @@ class TestEmailAccount(unittest.TestCase):
self.assertEqual(comm_list[0].reference_name, comm_list[1].reference_name)
def test_threading_by_message_id(self):
frappe.db.sql("""delete from tabCommunication""")
cleanup()
frappe.db.sql("""delete from `tabEmail Queue`""")
# reference document for testing
@ -196,3 +195,13 @@ class TestEmailAccount(unittest.TestCase):
# check if threaded correctly
self.assertEqual(comm_list[0].reference_doctype, event.doctype)
self.assertEqual(comm_list[0].reference_name, event.name)
def cleanup(sender=None):
filters = {}
if sender:
filters.update({"sender": sender})
names = frappe.get_list("Communication", filters=filters, fields=["name"])
for name in names:
frappe.delete_doc_if_exists("Communication", name.name)
frappe.delete_doc_if_exists("Communication Link", {"parent": name.name})

View file

@ -126,9 +126,14 @@ def get_context(context):
self.send_a_slack_msg(doc, context)
if self.set_property_after_alert:
frappe.db.set_value(doc.doctype, doc.name, self.set_property_after_alert,
self.property_value, update_modified = False)
doc.set(self.set_property_after_alert, self.property_value)
allow_update = True
if doc.docstatus == 1 and not doc.meta.get_field(self.set_property_after_alert).allow_on_submit:
allow_update = False
if allow_update:
frappe.db.set_value(doc.doctype, doc.name, self.set_property_after_alert,
self.property_value, update_modified = False)
doc.set(self.set_property_after_alert, self.property_value)
def send_an_email(self, doc, context):
from email.utils import formataddr
@ -150,6 +155,7 @@ def get_context(context):
reference_doctype = doc.doctype,
reference_name = doc.name,
attachments = attachments,
expose_recipients="header",
print_letterhead = ((attachments
and attachments[0].get('print_letterhead')) or False))

View file

@ -2330,6 +2330,7 @@
},
"Suriname": {
"code": "sr",
"currency": "SRD",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"currency_symbol": "$",

View file

@ -118,7 +118,6 @@ doc_events = {
"frappe.core.doctype.activity_log.feed.update_feed",
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
"frappe.automation.doctype.assignment_rule.assignment_rule.apply",
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points",
"frappe.automation.doctype.milestone_tracker.milestone_tracker.evaluate_milestone"
],
"after_rename": "frappe.desk.notifications.clear_doctype_notifications",
@ -131,8 +130,8 @@ doc_events = {
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions"
],
"on_change": [
"frappe.core.doctype.feedback_trigger.feedback_trigger.trigger_feedback_request"
]
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points"
],
},
"Email Group Member": {
"validate": "frappe.email.doctype.email_group.email_group.restrict_email_group"
@ -157,6 +156,7 @@ scheduler_events = {
"frappe.utils.error.collect_error_snapshots",
"frappe.desk.page.backups.backups.delete_downloadable_backups",
"frappe.limits.update_space_usage",
"frappe.limits.update_site_usage",
"frappe.desk.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry",
"frappe.deferred_insert.save_to_db",
"frappe.desk.form.document_follow.send_hourly_updates"
@ -172,7 +172,6 @@ scheduler_events = {
"frappe.utils.scheduler.disable_scheduler_on_expiry",
"frappe.utils.scheduler.restrict_scheduler_events_if_dormant",
"frappe.email.doctype.auto_email_report.auto_email_report.send_daily",
"frappe.core.doctype.feedback_request.feedback_request.delete_feedback_request",
"frappe.core.doctype.activity_log.activity_log.clear_authentication_logs",
"frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.remove_unverified_record",
"frappe.desk.form.document_follow.send_daily_updates",
@ -285,4 +284,4 @@ user_privacy_documents = [
'applies_to_website_user': 1
},
]
]

View file

@ -1,5 +1,7 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
@ -12,16 +14,20 @@
"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": "integration_type",
"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": "Integration Type",
@ -38,19 +44,24 @@
"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": "integration_request_service",
"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": "Integration Request Service",
@ -67,26 +78,31 @@
"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,
"default": "Queued",
"fetch_if_empty": 0,
"fieldname": "status",
"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": 1,
"label": "Status",
"length": 0,
"no_copy": 0,
"options": "\nQueued\nAuthorized\nCompleted\nCancelled\nFailed\n",
"options": "\nQueued\nAuthorized\nCompleted\nCancelled\nFailed",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@ -97,19 +113,24 @@
"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": "data",
"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": "Data",
@ -125,19 +146,24 @@
"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": "output",
"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": "Output",
@ -153,19 +179,24 @@
"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",
"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": "Error",
@ -181,19 +212,24 @@
"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": "reference_doctype",
"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": "Reference Doctype",
@ -210,19 +246,24 @@
"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": "reference_docname",
"fieldtype": "Dynamic 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": "Reference Docname",
@ -239,20 +280,21 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 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": "2019-10-09 14:40:00.783063",
"modified": "2019-04-25 16:38:21.084580",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Integration Request",
@ -261,7 +303,6 @@
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
@ -269,7 +310,6 @@
"export": 1,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 1,
"read": 1,
@ -284,9 +324,11 @@
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "integration_request_service",
"track_changes": 1,
"track_seen": 0
"track_seen": 0,
"track_views": 0
}

View file

@ -1,317 +1,363 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-09-22 04:16:48.829658",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "System",
"editable_grid": 1,
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-09-22 04:16:48.829658",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "System",
"editable_grid": 1,
"fields": [
{
"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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "ldap_server_url",
"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": "LDAP Server Url",
"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": "organizational_unit",
"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": "Organizational Unit",
"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": "base_dn",
"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": "Base Distinguished Name (DN)",
"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": "password",
"fieldtype": "Password",
"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": "Password for Base DN",
"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": "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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "ldap_search_string",
"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": "LDAP Search String",
"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": "ldap_first_name_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": "LDAP First Name 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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "ldap_email_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": "LDAP 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_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "ldap_username_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": "LDAP Username 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_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 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,
"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": "ldap_server_url",
"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": "LDAP Server Url",
"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": "organizational_unit",
"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": "Organizational Unit",
"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": "base_dn",
"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": "Base Distinguished Name (DN)",
"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": "password",
"fieldtype": "Password",
"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": "Password for Base DN",
"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": "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,
"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": "ldap_search_string",
"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": "LDAP Search String",
"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": "ldap_first_name_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": "LDAP First Name 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,
"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": "ldap_email_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": "LDAP 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,
"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": "ldap_username_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": "LDAP Username 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,
"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": "ldap_security",
"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": "LDAP Security",
"length": 0,
"no_copy": 0,
@ -325,22 +371,28 @@
"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,
"default": "Off",
"description": "",
"fetch_if_empty": 0,
"fieldname": "ssl_tls_mode",
"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": "SSL/TLS Mode",
"length": 0,
"no_copy": 0,
@ -355,21 +407,27 @@
"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,
"default": "No",
"fetch_if_empty": 0,
"fieldname": "require_trusted_certificate",
"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": "Require Trusted Certificate",
"length": 0,
"no_copy": 0,
@ -384,53 +442,153 @@
"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": "local_private_key_file",
"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": "Path to private Key File",
"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": "local_server_certificate_file",
"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": "Path to Server Certificate",
"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": "local_ca_certs_file",
"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": "Path to CA Certs File",
"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_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2019-01-30 11:02:41.011412",
"modified_by": "Administrator",
"module": "Integrations",
"name": "LDAP Settings",
"name_case": "",
"owner": "Administrator",
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2019-04-29 10:56:42.322696",
"modified_by": "Administrator",
"module": "Integrations",
"name": "LDAP Settings",
"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": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 1,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
],
"quick_entry": 0,
"read_only": 1,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View file

@ -5,56 +5,93 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import cstr
from frappe.model.document import Document
class LDAPSettings(Document):
def validate(self):
if not self.enabled:
return
if not self.flags.ignore_mandatory:
self.validate_ldap_credentails()
if self.ldap_search_string.endswith("={0}"):
if self.enabled:
connect_to_ldap(server_url=self.ldap_server_url,
base_dn=self.base_dn,
password=self.get_password(raise_exception=False),
ssl_tls_mode=self.ssl_tls_mode,
trusted_cert=self.require_trusted_certificate,
private_key_file=self.local_private_key_file,
server_cert_file=self.local_server_certificate_file,
ca_certs_file=self.local_ca_certs_file)
else:
frappe.throw(_("LDAP Search String needs to end with a placeholder, eg sAMAccountName={0}"))
def validate_ldap_credentails(self):
try:
import ldap
conn = ldap.initialize(self.ldap_server_url)
try:
if self.ssl_tls_mode == 'StartTLS':
conn.set_option(ldap.OPT_X_TLS_DEMAND, True)
if self.require_trusted_certificate == 'Yes':
conn.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND)
conn.start_tls_s()
except:
frappe.throw(_("StartTLS is not supported"))
conn.simple_bind_s(self.base_dn, self.get_password(raise_exception=False))
except ImportError:
msg = """
<div>
{{_("Seems ldap is not installed on system.<br>Guidelines to install ldap dependancies and python package")}},
<a href="https://discuss.erpnext.com/t/frappe-v-7-1-beta-ldap-dependancies/15841" target="_blank">{{_("Click here")}}</a>,
</div>
"""
frappe.throw(msg, title=_("LDAP Not Installed"))
def get_ldap_client_settings():
#return the settings to be used on the client side.
result = {
"enabled": False
}
settings = frappe.get_doc("LDAP Settings")
except ldap.LDAPError:
conn.unbind_s()
frappe.throw(_("Incorrect UserId or Password"))
if settings and settings.enabled:
result["enabled"] = True
result["method"] = "frappe.integrations.doctype.ldap_settings.ldap_settings.login"
return result
def get_ldap_settings():
def connect_to_ldap(server_url,
base_dn,
password,
ssl_tls_mode,
trusted_cert,
private_key_file,
server_cert_file,
ca_certs_file):
try:
settings = frappe.get_doc("LDAP Settings")
import ldap3
import ssl
if trusted_cert == 'Yes':
tls_configuration = ldap3.Tls(validate=ssl.CERT_REQUIRED,
version=ssl.PROTOCOL_TLSv1)
else:
tls_configuration = ldap3.Tls(validate=ssl.CERT_NONE,
version=ssl.PROTOCOL_TLSv1)
if private_key_file:
tls_configuration.private_key_file = private_key_file
if server_cert_file:
tls_configuration.certificate_file = server_cert_file
if ca_certs_file:
tls_configuration.ca_certs_file = ca_certs_file
server = ldap3.Server(host=server_url,
tls=tls_configuration)
bind_type = ldap3.AUTO_BIND_TLS_BEFORE_BIND if ssl_tls_mode == "StartTLS" else True
conn = ldap3.Connection(server=server,
user=base_dn,
password=password,
auto_bind=bind_type,
read_only=True,
raise_exceptions=True)
return conn
except ImportError:
msg = _("Please Install the ldap3 library via pip to use ldap functionality.")
frappe.throw(msg, title=_("LDAP Not Installed"))
except ldap3.core.exceptions.LDAPInvalidCredentialsResult:
frappe.throw(_("Invalid Credentials"))
except Exception as ex:
frappe.throw(_(str(ex)))
settings.update({
"method": "frappe.integrations.doctype.ldap_settings.ldap_settings.login"
})
return settings
except Exception:
# this will return blank settings
return frappe._dict()
@frappe.whitelist(allow_guest=True)
def login():
#### LDAP LOGIN LOGIC #####
# LDAP LOGIN LOGIC
args = frappe.form_dict
user = authenticate_ldap_user(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd))
@ -64,64 +101,57 @@ def login():
# because of a GET request!
frappe.db.commit()
def authenticate_ldap_user(user=None, password=None):
dn = None
def authenticate_ldap_user(user=None,
password=None):
params = {}
settings = get_ldap_settings()
settings = frappe.get_doc("LDAP Settings")
if settings and settings.enabled:
conn = connect_to_ldap(server_url=settings.ldap_server_url,
base_dn=settings.base_dn,
password=settings.get_password(raise_exception=False),
ssl_tls_mode=settings.ssl_tls_mode,
trusted_cert=settings.require_trusted_certificate,
private_key_file=settings.local_private_key_file,
server_cert_file=settings.local_server_certificate_file,
ca_certs_file=settings.local_ca_certs_file)
try:
import ldap
except:
msg = """
<div>
{{_("Seems ldap is not installed on system.")}}<br>
<a href"https://discuss.erpnext.com/t/frappe-v-7-1-beta-ldap-dependancies/15841">{{_("Click here")}}</a>,
{{_("Guidelines to install ldap dependancies and python")}}
</div>
"""
frappe.throw(msg, title=_("LDAP Not Installed"))
user_filter = settings.ldap_search_string.format(user)
conn.search(search_base=settings.organizational_unit,
search_filter="({0})".format(user_filter),
attributes=[settings.ldap_email_field,
settings.ldap_username_field,
settings.ldap_first_name_field])
conn = ldap.initialize(settings.ldap_server_url)
try:
try:
# set TLS settings for secure connection
if settings.ssl_tls_mode == 'StartTLS':
conn.set_option(ldap.OPT_X_TLS_DEMAND, True)
if settings.require_trusted_certificate == 'Yes':
conn.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND)
conn.start_tls_s()
except:
frappe.throw(_("StartTLS is not supported"))
# simple_bind_s is synchronous binding to server, it takes two param DN and password
conn.simple_bind_s(settings.base_dn, settings.get_password(raise_exception=False))
#search for surnames beginning with a
#available options for how deep a search you want.
#LDAP_SCOPE_BASE, LDAP_SCOPE_ONELEVEL,LDAP_SCOPE_SUBTREE,
result = conn.search_s(settings.organizational_unit, ldap.SCOPE_SUBTREE,
settings.ldap_search_string.format(user))
for dn, r in result:
dn = cstr(dn)
params["email"] = cstr(r[settings.ldap_email_field][0])
params["username"] = cstr(r[settings.ldap_username_field][0])
params["first_name"] = cstr(r[settings.ldap_first_name_field][0])
if dn:
conn.simple_bind_s(dn, frappe.as_unicode(password))
if len(conn.entries) > 0 and conn.entries[0]:
user = conn.entries[0]
params["email"] = str(user[settings.ldap_email_field])
params["username"] = str(user[settings.ldap_username_field])
params["first_name"] = str(user[settings.ldap_first_name_field])
connect_to_ldap(server_url=settings.ldap_server_url,
base_dn=user.entry_dn,
password=frappe.as_unicode(password),
ssl_tls_mode=settings.ssl_tls_mode,
trusted_cert=settings.require_trusted_certificate,
private_key_file=settings.local_private_key_file,
server_cert_file=settings.local_server_certificate_file,
ca_certs_file=settings.local_ca_certs_file
)
return create_user(params)
else:
frappe.throw(_("Not a valid LDAP user"))
else:
frappe.throw(_("LDAP is not enabled."))
except ldap.LDAPError:
conn.unbind_s()
frappe.throw(_("Incorrect UserId or Password"))
def create_user(params):
if frappe.db.exists("User", params["email"]):
return frappe.get_doc("User", params["email"])
user = frappe.get_doc("User", params["email"])
user.first_name = params["first_name"]
user.username = params["username"]
user.save(ignore_permissions=True)
return user
else:
params.update({
@ -135,6 +165,5 @@ def create_user(params):
})
user = frappe.get_doc(params).insert(ignore_permissions=True)
frappe.db.commit()
return user

View file

@ -5,7 +5,8 @@ from frappe.utils import now_datetime, getdate, flt, cint, get_fullname
from frappe.installer import update_site_config
from frappe.utils.data import formatdate
from frappe.utils.user import get_enabled_system_users, disable_users
import os, subprocess
from frappe.utils.__init__ import get_site_info
import os, subprocess, json
from six.moves.urllib.parse import parse_qsl, urlsplit, urlunsplit, urlencode
from six import string_types
@ -225,3 +226,8 @@ def get_folder_size(path):
if os.path.exists(path):
return flt(subprocess.check_output(['du', '-ms', path]).split()[0], 2)
def update_site_usage():
data = get_site_info()
with open(os.path.join(frappe.get_site_path(), 'site_data.json'), 'w') as outfile:
json.dump(data, outfile)
outfile.close()

View file

@ -76,27 +76,50 @@ def delete_fields(args_dict, delete=0):
args_dict = { dt: [field names] }
"""
import frappe.utils
for dt in list(args_dict):
for dt in args_dict:
fields = args_dict[dt]
if not fields: continue
if not fields:
continue
frappe.db.sql("""\
frappe.db.sql("""
DELETE FROM `tabDocField`
WHERE parent=%s AND fieldname IN (%s)
""" % ('%s', ", ".join(['"' + f + '"' for f in fields])), dt)
WHERE parent='%s' AND fieldname IN (%s)
""" % (dt, ", ".join(["'{}'".format(f) for f in fields])))
# Delete the data / column only if delete is specified
if not delete: continue
# Delete the data/column only if delete is specified
if not delete:
continue
if frappe.db.get_value("DocType", dt, "issingle"):
frappe.db.sql("""\
frappe.db.sql("""
DELETE FROM `tabSingles`
WHERE doctype=%s AND field IN (%s)
""" % ('%s', ", ".join(['"' + f + '"' for f in fields])), dt)
WHERE doctype='%s' AND field IN (%s)
""" % (dt, ", ".join(["'{}'".format(f) for f in fields])))
else:
existing_fields = frappe.db.sql("desc `tab%s`" % dt)
existing_fields = frappe.db.multisql({
"mariadb": "DESC `tab%s`" % dt,
"postgres": """
SELECT
COLUMN_NAME
FROM
information_schema.COLUMNS
WHERE
TABLE_NAME = 'tab%s';
""" % dt,
})
existing_fields = existing_fields and [e[0] for e in existing_fields] or []
fields_need_to_delete = set(fields) & set(existing_fields)
if not fields_need_to_delete:
continue
if frappe.conf.db_type == 'mariadb':
# mariadb implicitly commits before DDL, make it explicit
frappe.db.commit()
query = "ALTER TABLE `tab%s` " % dt + \
", ".join(["DROP COLUMN `%s`" % f for f in fields if f in existing_fields])
frappe.db.commit()
", ".join(["DROP COLUMN `%s`" % f for f in fields_need_to_delete])
frappe.db.sql(query)
if frappe.conf.db_type == 'postgres':
# commit the results to db
frappe.db.commit()

View file

@ -197,7 +197,7 @@ class BaseDocument(object):
return value
def get_valid_dict(self, sanitize=True, convert_dates_to_str=False):
def get_valid_dict(self, sanitize=True, convert_dates_to_str=False, ignore_nulls = False):
d = frappe._dict()
for fieldname in self.meta.get_valid_columns():
d[fieldname] = self.get(fieldname)
@ -234,6 +234,9 @@ class BaseDocument(object):
if convert_dates_to_str and isinstance(d[fieldname], (datetime.datetime, datetime.time, datetime.timedelta)):
d[fieldname] = str(d[fieldname])
if d[fieldname] == None and ignore_nulls:
del d[fieldname]
return d
def init_valid_columns(self):
@ -306,7 +309,8 @@ class BaseDocument(object):
self.creation = self.modified = now()
self.created_by = self.modified_by = frappe.session.user
d = self.get_valid_dict(convert_dates_to_str=True)
# if doctype is "DocType", don't insert null values as we don't know who is valid yet
d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in ('DocType', 'DocField', 'DocPerm'))
columns = list(d)
try:
@ -341,7 +345,7 @@ class BaseDocument(object):
self.db_insert()
return
d = self.get_valid_dict(convert_dates_to_str=True)
d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in ('DocType', 'DocField', 'DocPerm'))
# don't update name, as case might've been changed
name = d['name']

View file

@ -66,6 +66,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
frappe.db.sql("delete from `tabProperty Setter` where doc_type = %s", name)
frappe.db.sql("delete from `tabReport` where ref_doctype=%s", name)
frappe.db.sql("delete from `tabCustom DocPerm` where parent=%s", name)
frappe.db.sql("delete from `__global_search` where doctype=%s", name)
delete_from_table(doctype, name, ignore_doctypes, None)
@ -277,9 +278,8 @@ def delete_dynamic_links(doctype, name):
delete_references('Document Follow', doctype, name, 'ref_doctype', 'ref_docname')
# unlink communications
clear_timeline_references(doctype, name)
clear_references('Communication', doctype, name)
clear_references('Communication', doctype, name, 'link_doctype', 'link_name')
clear_references('Communication', doctype, name, 'timeline_doctype', 'timeline_name')
clear_references('Activity Log', doctype, name)
clear_references('Activity Log', doctype, name, 'timeline_doctype', 'timeline_name')
@ -300,6 +300,9 @@ def clear_references(doctype, reference_doctype, reference_name,
{1}=%s and {2}=%s'''.format(doctype, reference_doctype_field, reference_name_field), # nosec
(reference_doctype, reference_name))
def clear_timeline_references(link_doctype, link_name):
frappe.db.sql("""delete from `tabCommunication Link`
where `tabCommunication Link`.link_doctype='{0}' and `tabCommunication Link`.link_name='{1}'""".format(link_doctype, link_name)) # nosec
def insert_feed(doc):
from frappe.utils import get_fullname

View file

@ -230,6 +230,9 @@ class Document(BaseDocument):
self.set_docstatus()
self.flags.in_insert = False
# follow document on document creation
# run validate, on update etc.
# parent
@ -259,6 +262,8 @@ class Document(BaseDocument):
if hasattr(self, "__islocal"):
delattr(self, "__islocal")
if not (frappe.flags.in_migrate or frappe.local.flags.in_install):
follow_document(self.doctype, self.name, frappe.session.user)
return self
def save(self, *args, **kwargs):
@ -1190,18 +1195,11 @@ class Document(BaseDocument):
return
# update timeline doc in communication if it is different than current timeline doc
frappe.db.sql("""update `tabCommunication`
set timeline_doctype=%(timeline_doctype)s, timeline_name=%(timeline_name)s
where
reference_doctype=%(doctype)s and reference_name=%(name)s
and (timeline_doctype is null or timeline_doctype != %(timeline_doctype)s
or timeline_name is null or timeline_name != %(timeline_name)s)""",
{
"doctype": self.doctype,
"name": self.name,
"timeline_doctype": timeline_doctype,
"timeline_name": timeline_name
})
communications = frappe.get_list("Communication", filters={"reference_doctype": self.doctype, "reference_name": self.name})
for communication in communications:
communication = frappe.get_doc("Communication", communication.name)
# duplicate entries will be handled by deduplicate links in communication
communication.add_link(link_doctype=timeline_doctype, link_name=timeline_name, autosave=True)
def queue_action(self, action, **kwargs):
'''Run an action in background. If the action has an inner function,

View file

@ -161,7 +161,7 @@ def validate_rename(doctype, new, meta, merge, force, ignore_permissions):
if (not merge) and exists:
frappe.msgprint(_("Another {0} with name {1} exists, select another name").format(doctype, new), raise_exception=1)
if not (ignore_permissions or frappe.has_permission(doctype, "write")):
if not (ignore_permissions or frappe.permissions.has_permission(doctype, "write", raise_exception=False)):
frappe.msgprint(_("You need write permission to rename"), raise_exception=1)
if not (force or ignore_permissions) and not meta.allow_rename:

View file

@ -230,9 +230,10 @@ frappe.patches.v11_0.delete_all_prepared_reports
frappe.patches.v11_0.fix_order_by_in_reports_json
execute:frappe.delete_doc('Page', 'applications', ignore_missing=True)
frappe.patches.v11_0.set_missing_creation_and_modified_value_for_user_permissions
frappe.patches.v11_0.remove_doctype_user_permissions_for_page_and_report
frappe.patches.v11_0.set_default_letter_head_source
frappe.patches.v12_0.set_primary_key_in_series
execute:frappe.reload_doc('core', 'doctype', 'communication_link')
execute:frappe.reload_doc('core', 'doctype', 'communication')
execute:frappe.delete_doc("Page", "modules", ignore_missing=True)
frappe.patches.v11_0.set_default_letter_head_source
frappe.patches.v12_0.setup_comments_from_communications
@ -240,3 +241,7 @@ frappe.patches.v12_0.init_desk_settings #11-03-2019
frappe.patches.v12_0.replace_null_values_in_tables
frappe.patches.v12_0.reset_home_settings
frappe.patches.v12_0.update_print_format_type
frappe.patches.v11_0.remove_doctype_user_permissions_for_page_and_report #2019-05-01
frappe.patches.v12_0.remove_feedback_rating
frappe.patches.v12_0.move_form_attachments_to_attachments_folder
frappe.patches.v12_0.move_timeline_links_to_dynamic_links

View file

@ -6,6 +6,7 @@ import re
def execute():
""" Create Contact for each User if not present """
frappe.reload_doc('contacts', 'doctype', 'contact')
frappe.reload_doc('core', 'doctype', 'dynamic_link')
users = frappe.get_all('User', filters={"name": ('not in', 'Administrator, Guest')}, fields=["*"])
for user in users:

View file

@ -5,5 +5,4 @@ from __future__ import unicode_literals
import frappe
def execute():
if frappe.db.table_exists('User Permission for Page and Report'):
frappe.delete_doc("DocType", "User Permission for Page and Report")
frappe.delete_doc_if_exists("DocType", "User Permission for Page and Report")

View file

@ -6,4 +6,4 @@ def execute():
frappe.reload_doctype('Letter Head')
# source of all existing letter heads must be HTML
frappe.db.sql('update `tabLetter Head` set source = "HTML"')
frappe.db.sql("update `tabLetter Head` set source = 'HTML'")

View file

@ -8,4 +8,4 @@ from frappe.desk.moduleview import get_onboard_items
def execute():
"""Reset the initial customizations for desk, with modules, indices and links."""
frappe.reload_doc("core", "doctype", "user")
frappe.db.sql("""update tabUser set home_settings = %s""", (''), debug=True)
frappe.db.sql("""update `tabUser` set home_settings = %s""", (''), debug=True)

View file

@ -0,0 +1,9 @@
import frappe
def execute():
frappe.db.sql('''
UPDATE tabFile
SET folder = 'Home/Attachments'
WHERE ifnull(attached_to_doctype, '') != ''
AND folder = 'Home'
''')

View file

@ -0,0 +1,44 @@
from __future__ import unicode_literals
import frappe
def execute():
communications = frappe.db.sql("""
SELECT
`tabCommunication`.name, `tabCommunication`.creation, `tabCommunication`.modified,
`tabCommunication`.modified_by,`tabCommunication`.timeline_doctype, `tabCommunication`.timeline_name,
`tabCommunication`.link_doctype, `tabCommunication`.link_name
FROM `tabCommunication`
WHERE `tabCommunication`.communication_medium='Email'
""", as_dict=True)
name = 1000000000
values = []
for count, communication in enumerate(communications):
counter = 1
if communication.timeline_doctype and communication.timeline_name:
name += 1
values.append("""({0}, "{1}", "timeline_links", "Communication", "{2}", "{3}", "{4}", "{5}", "{6}", "{7}")""".format(
counter, str(name), communication.name, communication.timeline_doctype,
communication.timeline_name, communication.creation, communication.modified, communication.modified_by
))
counter += 1
if communication.link_doctype and communication.link_name:
name += 1
values.append("""({0}, "{1}", "timeline_links", "Communication", "{2}", "{3}", "{4}", "{5}", "{6}", "{7}")""".format(
counter, str(name), communication.name, communication.link_doctype,
communication.link_name, communication.creation, communication.modified, communication.modified_by
))
if values and (count % 10000 == 0 or count == len(communications) - 1):
frappe.db.sql("""
INSERT INTO `tabCommunication Link`
(`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`,
`modified`, `modified_by`)
VALUES {0}
""".format(", ".join([d for d in values])))
values = []
frappe.db.add_index("Communication Link", ["link_doctype", "link_name"])

View file

@ -0,0 +1,9 @@
import frappe
def execute():
'''
Deprecate Feedback Trigger and Rating. This feature was not customizable.
Now can be achieved via custom Web Forms
'''
frappe.delete_doc('DocType', 'Feedback Trigger')
frappe.delete_doc('DocType', 'Feedback Rating')

View file

@ -25,4 +25,4 @@ def execute():
new_comment.db_insert()
# clean up
frappe.db.sql('delete from tabCommunication where communication_type = "Comment"')
frappe.db.sql("delete from `tabCommunication` where communication_type = 'Comment'")

View file

@ -3,11 +3,11 @@ import frappe
def execute():
frappe.db.sql('''
UPDATE `tabPrint Format`
SET `print_format_type` = "Jinja"
WHERE `print_format_type` in ("Server", "Client")
SET `print_format_type` = 'Jinja'
WHERE `print_format_type` in ('Server', 'Client')
''')
frappe.db.sql('''
UPDATE `tabPrint Format`
SET `print_format_type` = "JS"
WHERE `print_format_type` = "Js"
SET `print_format_type` = 'JS'
WHERE `print_format_type` = 'Js'
''')

View file

@ -7,18 +7,4 @@ def execute():
remove Guest None from sender full name
setup feedback request trigger's is_manual field
"""
frappe.reload_doc('core', 'doctype', 'dynamic_link')
frappe.reload_doc('email', 'doctype', 'contact')
frappe.reload_doc("core", "doctype", "feedback_request")
frappe.reload_doc("core", "doctype", "communication")
if frappe.db.has_column('Communication', 'feedback'):
frappe.db.sql("""update tabCommunication set content=ifnull(feedback, "feedback details not provided")
where communication_type="Feedback" and content is NULL""")
frappe.db.sql(""" update tabCommunication set sender_full_name="" where communication_type="Feedback"
and sender_full_name='Guest None' """)
frappe.db.sql(""" update `tabFeedback Request` set is_manual=1, feedback_trigger="Manual"
where ifnull(feedback_trigger, '')='' """)
return

View file

@ -7,29 +7,4 @@ def execute():
update the feedback request and save the rating and communication
reference in Feedback Request document
"""
frappe.reload_doc("core", "doctype", "feedback_request")
feedback_requests = frappe.get_all("Feedback Request")
for request in feedback_requests:
communication, rating = frappe.db.get_value("Communication", { "feedback_request": request.get("name") },
["name", "rating"]) or [None, 0]
if communication:
frappe.db.sql("""update `tabFeedback Request` set reference_communication='{communication}',
rating={rating} where name='{feedback_request}'""".format(
communication=communication,
rating=rating or 0,
feedback_request=request.get("name")
))
if "Feedback" not in request.get("name"):
# rename the feedback request doc
reference_name, creation = frappe.db.get_value("Feedback Request", request.get("name"), ["name", "creation"])
oldname = request.get("name")
newname = "Feedback for {doctype} {docname} on {datetime}".format(
doctype="Feedback Request",
docname=reference_name,
datetime=creation
)
frappe.rename_doc("Feedback Request", oldname, newname, ignore_permissions=True)
if communication: frappe.db.set_value("Communication", communication, "feedback_request", newname)
return

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