Merge barnch develop into mandatory-depends-on

This commit is contained in:
deepeshgarg007 2019-12-12 20:58:27 +05:30
commit 33ec59ad53
59 changed files with 581 additions and 367 deletions

View file

@ -1,9 +1,15 @@
context('Form', () => {
before(() => {
beforeEach(() => {
cy.login();
cy.visit('/desk');
});
before(() => {
cy.login();
cy.visit('/desk');
cy.window().its('frappe').then(frappe => {
frappe.call("frappe.tests.ui_test_helpers.create_contact_records");
});
});
it('create a new form', () => {
cy.visit('/desk#Form/ToDo/New ToDo 1');
cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
@ -13,4 +19,21 @@ context('Form', () => {
cy.location('hash').should('eq', '#List/ToDo/List');
cy.get('.list-row').should('contain', 'this is a test todo');
});
it('navigates between documents with child table list filters applied', () => {
cy.visit('/desk#List/Contact');
cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
cy.get('.fieldname-select-area').should('exist');
cy.get('.fieldname-select-area input').type('Number{enter}', { force: true });
cy.get('.filter-field .input-with-feedback.form-control').type('123', { force: true });
cy.get('.filter-box .btn:contains("Apply")').click({ force: true });
cy.visit('/desk#Form/Contact/Test Form Contact 3');
cy.get('.prev-doc').click();
cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible');
cy.get('.modal-backdrop').click();
cy.get('.next-doc').click();
cy.contains('Test Form Contact 2').should('not.exist');
cy.get('.page-title .title-text').should('contain', 'Test Form Contact 1');
cy.visit('/desk#List/Contact');
cy.get('.clear-filters.btn').click();
});
});

View file

@ -13,6 +13,8 @@ from six import text_type
@click.argument('site')
@click.option('--db-name', help='Database name')
@click.option('--db-type', default='mariadb', type=click.Choice(['mariadb', 'postgres']), help='Optional "postgres" or "mariadb". Default is "mariadb"')
@click.option('--db-host', help='Database Host')
@click.option('--db-port', type=int, help='Database Port')
@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB')
@click.option('--mariadb-root-password', help='Root password for MariaDB')
@click.option('--admin-password', help='Administrator password for new site', default=None)
@ -21,22 +23,22 @@ from six import text_type
@click.option('--source_sql', help='Initiate database with a SQL file')
@click.option('--install-app', multiple=True, help='Install app after installation')
def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None,
verbose=False, install_apps=None, source_sql=None, force=None, install_app=None,
db_name=None, db_type=None):
verbose=False, install_apps=None, source_sql=None, force=None, install_app=None,
db_name=None, db_type=None, db_host=None, db_port=None):
"Create a new site"
frappe.init(site=site, new_site=True)
_new_site(db_name, site, mariadb_root_username=mariadb_root_username,
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
db_type=db_type)
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
db_type=db_type, db_host=db_host, db_port=db_port)
if len(frappe.utils.get_sites()) == 1:
use(site)
def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None,
admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False,
reinstall=False, db_type=None):
admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False,
reinstall=False, db_type=None, db_host=None, db_port=None):
"""Install a new Frappe site"""
if not force and os.path.exists(site):
@ -65,8 +67,8 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
installing = touch_file(get_site_path('locks', 'installing.lock'))
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password,
db_name=db_name, admin_password=admin_password, verbose=verbose,
source_sql=source_sql, force=force, reinstall=reinstall, db_type=db_type)
db_name=db_name, admin_password=admin_password, verbose=verbose,
source_sql=source_sql, force=force, reinstall=reinstall, db_type=db_type, db_host=db_host, db_port=db_port)
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
for app in apps_to_install:

View file

@ -60,10 +60,6 @@ class Address(Document):
if not [row for row in self.links if row.link_doctype == "Company"]:
frappe.throw(_("Company is mandatory, as it is your company address"))
# removing other links
to_remove = [row for row in self.links if row.link_doctype != "Company"]
[ self.remove(row) for row in to_remove ]
def get_display(self):
return get_address_display(self.as_dict())

View file

@ -398,7 +398,7 @@ def get_bcc(doc, recipients=None, fetched_from_email_account=False):
return bcc
def add_attachments(name, attachments):
'''Add attachments to the given Communiction'''
'''Add attachments to the given Communication'''
# loop through attachments
for a in attachments:
if isinstance(a, string_types):
@ -411,7 +411,9 @@ def add_attachments(name, attachments):
"file_url": attach.file_url,
"attached_to_doctype": "Communication",
"attached_to_name": name,
"folder": "Home/Attachments"})
"folder": "Home/Attachments",
"is_private": attach.is_private
})
_file.save(ignore_permissions=True)
def filter_email_list(doc, email_list, exclude, is_cc=False, is_bcc=False):

View file

@ -89,8 +89,9 @@ class File(Document):
def validate(self):
if self.is_new():
self.set_is_private()
self.set_file_name()
self.validate_duplicate_entry()
self.validate_file_name()
self.validate_folder()
if not self.file_url and not self.flags.ignore_file_validate:
@ -133,6 +134,9 @@ class File(Document):
frappe.db.set_value(self.attached_to_doctype, self.attached_to_name,
self.attached_to_field, self.file_url)
if self.file_url and (self.is_private != self.file_url.startswith('/private')):
frappe.throw(_('Invalid file URL. Please contact System Administrator.'))
def set_folder_name(self):
"""Make parent folders if not exists based on reference doctype and name"""
if self.attached_to_doctype and not self.folder:
@ -157,9 +161,11 @@ class File(Document):
def validate_duplicate_entry(self):
if not self.flags.ignore_duplicate_entry_error and not self.is_folder:
# check duplicate name
if not self.content_hash:
self.generate_content_hash()
# check duplicate assignement
# check duplicate name
# check duplicate assignment
filters = {
'content_hash': self.content_hash,
'is_private': self.is_private,
@ -184,21 +190,20 @@ class File(Document):
else:
self.file_url = duplicate_file.file_url
def validate_file_name(self):
def set_file_name(self):
if not self.file_name and self.file_url:
self.file_name = self.file_url.split('/')[-1]
def generate_content_hash(self):
if self.content_hash or not self.file_url:
if self.content_hash or not self.file_url or self.file_url.startswith('http'):
return
if self.file_url.startswith("/files/"):
try:
with open(get_files_path(self.file_name.lstrip("/")), "rb") as f:
self.content_hash = get_content_hash(f.read())
except IOError:
frappe.msgprint(_("File {0} does not exist").format(self.file_url))
raise
try:
with open(get_files_path(self.file_name.lstrip("/"), is_private=self.is_private), "rb") as f:
self.content_hash = get_content_hash(f.read())
except IOError:
frappe.msgprint(_("File {0} does not exist").format(self.file_url))
raise
def on_trash(self):
if self.is_home_folder or self.is_attachments_folder:
@ -563,6 +568,9 @@ class File(Document):
except frappe.DoesNotExistError:
frappe.clear_messages()
def set_is_private(self):
if self.file_url:
self.is_private = cint(self.file_url.startswith('/private'))
def on_doctype_update():
frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"])

View file

@ -60,7 +60,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Frequency",
"options": "All\nHourly\nDaily\nDaily Long\nWeekly\nWeekly Long\nMonthly\nMonthly Long\nCron\nYearly\nAnnual",
"options": "All\nHourly\nHourly Long\nDaily\nDaily Long\nWeekly\nWeekly Long\nMonthly\nMonthly Long\nCron\nYearly\nAnnual",
"read_only": 1,
"reqd": 1
}
@ -72,7 +72,7 @@
"link_fieldname": "scheduled_job_type"
}
],
"modified": "2019-09-27 12:19:23.259989",
"modified": "2019-12-09 11:10:21.259929",
"modified_by": "Administrator",
"module": "Core",
"name": "Scheduled Job Type",

View file

@ -23,7 +23,6 @@ class ScheduledJobType(Document):
def enqueue(self):
# enqueue event if last execution is done
if self.is_event_due():
self.update_last_execution()
if frappe.flags.enqueued_jobs:
frappe.flags.enqueued_jobs.append(self.method)
@ -39,11 +38,8 @@ class ScheduledJobType(Document):
def is_event_due(self, current_time = None):
'''Return true if event is due based on time lapsed since last execution'''
# save last execution in expected execution time as per cron
self.last_execution = self.get_next_execution()
# if the next scheduled event is before NOW, then its due!
return self.last_execution <= (current_time or now_datetime())
return self.get_next_execution() <= (current_time or now_datetime())
def is_job_in_queue(self):
queued_jobs = get_jobs(site=frappe.local.site, key='job_type')[frappe.local.site]
@ -68,7 +64,7 @@ class ScheduledJobType(Document):
self.cron_format = CRON_MAP[self.frequency]
return croniter(self.cron_format,
get_datetime(self.last_execution)).get_next(datetime)
get_datetime(self.last_execution or datetime(2000, 1, 1))).get_next(datetime)
def execute(self):
self.scheduler_log = None
@ -94,15 +90,16 @@ class ScheduledJobType(Document):
self.scheduler_log.db_set('status', status)
if status == 'Failed':
self.scheduler_log.db_set('details', frappe.get_traceback())
frappe.db.commit()
def update_last_execution(self):
self.db_set('last_execution', self.last_execution, update_modified=False)
if status == 'Start':
self.db_set('last_execution', now_datetime(), update_modified=False)
frappe.db.commit()
def get_queue_name(self):
return 'long' if ('Long' in self.frequency) else 'default'
def on_trash(self):
frappe.db.sql('delete from `tabScheduled Job Log` where scheduled_job_type=%s', self.name)
@frappe.whitelist()
def execute_event(doc):
frappe.only_for('System Manager')

View file

@ -97,7 +97,9 @@ class User(Document):
self.share_with_self()
clear_notifications(user=self.name)
frappe.clear_cache(user=self.name)
self.send_password_notification(self.__new_password)
if self.__new_password:
self.send_password_notification(self.__new_password)
self.reset_password_key = ''
create_contact(self, ignore_mandatory=True)
if self.name not in ('Administrator', 'Guest') and not self.user_image:
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name)
@ -1071,4 +1073,4 @@ def generate_keys(user):
user_details.save()
return {"api_secret": api_secret}
frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)
frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)

View file

@ -11,7 +11,7 @@
<tbody>
{% for j in jobs %}
<tr>
<td><span class="indicator {{ j.color }}" title="{{ j.status }}">{{ j.queue.split(".").slice(-1)[0] }}</span></td>
<td><span class="indicator {{ j.color }}" title="{{ j.get_status() }}">{{ j.queue.split(".").slice(-1)[0] }}</span></td>
<td style="overflow: auto;">
<div>
{{ frappe.utils.encode_tags(j.job_name) }}

View file

@ -29,9 +29,9 @@ def get_info(show_failed=False):
jobs.append({
'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') \
or str(j.kwargs.get('job_name')),
'status': j.status, 'queue': name,
'status': j.get_status(), 'queue': name,
'creation': format_datetime(convert_utc_to_user_timezone(j.created_at)),
'color': colors[j.status]
'color': colors[j.get_status()]
})
if j.exc_info:
jobs[-1]['exc_info'] = j.exc_info

View file

@ -80,12 +80,14 @@ class DbManager:
if pipe:
print('Creating Database...')
command = '{pipe} mysql -u {user} -p{password} -h{host} {target} {source}'.format(
command = '{pipe} mysql -u {user} -p{password} -h{host} ' + ('-P{port}' if frappe.db.port else '') + ' {target} {source}'
command = command.format(
pipe=pipe,
user=esc(user),
password=esc(password),
host=esc(frappe.db.host),
target=esc(target),
source=source
source=source,
port=frappe.db.port
)
os.system(command)

View file

@ -29,12 +29,16 @@ class GlobalSearchSettings(Document):
repeated_dts = (", ".join([frappe.bold(dt) for dt in repeated_dts]))
frappe.throw(_("Document Type {0} has been repeated.").format(repeated_dts))
def get_doctypes_for_global_search():
doctypes = frappe.get_list("Global Search DocType", fields=["document_type"], order_by="idx ASC")
if not doctypes:
return []
# reset cache
frappe.cache().hdel('global_search', 'search_priorities')
def get_doctypes_for_global_search():
def get_from_db():
doctypes = frappe.get_list("Global Search DocType", fields=["document_type"], order_by="idx ASC")
return [d.document_type for d in doctypes] or []
return frappe.cache().hget("global_search", "search_priorities", get_from_db)
return [d.document_type for d in doctypes]
@frappe.whitelist()
def reset_global_search_settings_doctypes():
@ -57,7 +61,7 @@ def update_global_search_doctypes():
if search_doctypes.get(domain):
global_search_doctypes.extend(search_doctypes.get(domain))
doctype_list = set([dt.name for dt in frappe.get_list("DocType")])
doctype_list = set([dt.name for dt in frappe.get_all("DocType")])
allowed_in_global_search = []
for dt in global_search_doctypes:

View file

@ -1,10 +1,10 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Setup Wizard Slide', {
frappe.ui.form.on('Onboarding Slide', {
refresh: function(frm) {
frm.toggle_reqd('ref_doctype', frm.doc.slide_type!='Information');
frm.toggle_reqd('slide_module', frm.doc.slide_type=='Information');
frm.toggle_reqd('ref_doctype', (frm.doc.slide_type=='Create' || frm.doc.slide_type=='Settings'));
frm.toggle_reqd('slide_module', (frm.doc.slide_type=='Information' || frm.doc.slide_type=='Continue'));
},
ref_doctype: function(frm) {
@ -33,7 +33,7 @@ frappe.ui.form.on('Setup Wizard Slide', {
reqd: 1
});
$.each(fields, function(_i, data) {
let row = frappe.model.add_child(frm.doc, 'Setup Wizard Slide', 'slide_fields');
let row = frappe.model.add_child(frm.doc, 'Onboarding Slide', 'slide_fields');
row.label = data.label;
row.fieldtype = data.fieldtype;
row.fieldname = data.fieldname;

View file

@ -15,7 +15,6 @@
"slide_desc",
"action_section_break",
"slide_type",
"submit_method",
"column_break_6",
"max_count",
"add_more_button",
@ -25,7 +24,8 @@
"section_break_10",
"domains",
"column_break_12",
"help_links"
"help_links",
"is_completed"
],
"fields": [
{
@ -36,13 +36,6 @@
"reqd": 1,
"unique": 1
},
{
"depends_on": "eval:doc.slide_type!='Information'",
"description": "By default the code inside `create_onboarding_docs` method of the `Reference Document Type` is executed. If your method is not on the doctype level, place this method in {app_name}.utilities.onboarding_utils.{method_name} and specify the method name here",
"fieldname": "submit_method",
"fieldtype": "Data",
"label": "Submit Method"
},
{
"fieldname": "slide_desc",
"fieldtype": "HTML Editor",
@ -58,17 +51,17 @@
},
{
"default": "0",
"depends_on": "eval:doc.slide_type!='Information'",
"depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'",
"fieldname": "add_more_button",
"fieldtype": "Check",
"label": "Add More Button"
},
{
"depends_on": "eval:doc.slide_type!='Information'",
"depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'",
"fieldname": "slide_fields",
"fieldtype": "Table",
"label": "Slide Fields",
"options": "Setup Wizard Slide Field"
"options": "Onboarding Slide Field"
},
{
"fieldname": "section_break_10",
@ -90,7 +83,7 @@
"fieldname": "help_links",
"fieldtype": "Table",
"label": "Help Links",
"options": "Setup Wizard Help Link"
"options": "Onboarding Slide Help Link"
},
{
"fieldname": "action_section_break",
@ -98,11 +91,11 @@
"label": "Action Settings"
},
{
"description": "If slide type is Action there should be a submit method bound to be executed after the slide is completed.",
"description": "If Slide Type is Create or Settings there should be a 'create_onboarding_docs' method in the {ref_doctype}.py file bound to be executed after the slide is completed.",
"fieldname": "slide_type",
"fieldtype": "Select",
"label": "Slide Type",
"options": "Information\nCreate\nSettings",
"options": "Information\nCreate\nSettings\nContinue",
"reqd": 1
},
{
@ -131,7 +124,7 @@
"label": "Description"
},
{
"depends_on": "eval:doc.slide_type!='Information'",
"depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'",
"fieldname": "ref_doctype",
"fieldtype": "Link",
"label": "Reference Document Type",
@ -145,22 +138,31 @@
"label": "Slide Order"
},
{
"depends_on": "eval:doc.slide_type=='Information'",
"depends_on": "eval:doc.slide_type=='Information' || doc.slide_type=='Continue'",
"fieldname": "slide_module",
"fieldtype": "Link",
"label": "Module",
"options": "Module Def"
},
{
"collapsible_depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'",
"fieldname": "section_break_18",
"fieldtype": "Section Break",
"label": "Fields"
},
{
"default": "0",
"fieldname": "is_completed",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Completed",
"print_hide": 1
}
],
"modified": "2019-11-26 17:33:34.553367",
"modified": "2019-12-04 10:50:43.528901",
"modified_by": "Administrator",
"module": "Desk",
"name": "Setup Wizard Slide",
"name": "Onboarding Slide",
"owner": "Administrator",
"permissions": [
{

View file

@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json
from frappe import _
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
class OnboardingSlide(Document):
def validate(self):
if self.slide_type == 'Continue' and frappe.db.exists('Onboarding Slide', {'slide_type': 'Continue', 'name': ('!=', self.name)}):
frappe.throw(_('An Onboarding Slide of Slide Type Continue already exists.'))
if self.slide_order:
same_order_slide = frappe.db.exists('Onboarding Slide', {'slide_order': self.slide_order, 'name': ('!=', self.name)})
if same_order_slide:
frappe.throw(_('An Onboarding Slide <b>{0}</b> with the same slide order already exists').format(same_order_slide))
def on_update(self):
if self.ref_doctype:
module = frappe.db.get_value('DocType', self.ref_doctype, 'module')
else:
module = self.slide_module
export_to_files(record_list=[['Onboarding Slide', self.name]], record_module=module)
def get_onboarding_slides_as_list():
slides = []
slide_docs = frappe.db.get_all('Onboarding Slide',
filters={'is_completed': 0},
or_filters={'slide_order': ('!=', 0), 'slide_type': 'Continue'},
order_by='slide_order')
# to check if continue slide is required
first_slide = get_first_slide()
for entry in slide_docs:
# using get_doc because child table fields are not fetched in get_all
slide_doc = frappe.get_doc('Onboarding Slide', entry.name)
if frappe.scrub(slide_doc.app) in frappe.get_installed_apps():
slide = frappe._dict(
slide_type=slide_doc.slide_type,
title=slide_doc.slide_title,
help=slide_doc.slide_desc,
fields=slide_doc.slide_fields,
help_links=get_help_links(slide_doc),
add_more=slide_doc.add_more_button,
max_count=slide_doc.max_count,
image_src=get_slide_image(slide_doc),
ref_doctype=slide_doc.ref_doctype,
app=slide_doc.app
)
if slide.slide_type == 'Continue':
if is_continue_slide_required(first_slide):
slides.insert(0, slide)
else:
slides.append(slide)
return slides
@frappe.whitelist()
def get_onboarding_slides():
slides = []
slide_list = get_onboarding_slides_as_list()
active_domains = frappe.get_active_domains()
for slide in slide_list:
if not slide.domains or any(domain in active_domains for domain in slide.domains):
slides.append(slide)
return slides
def get_help_links(slide_doc):
links=[]
for link in slide_doc.help_links:
links.append({
'label': link.label,
'video_id': link.video_id
})
return links
def get_slide_image(slide_doc):
if slide_doc.image_src:
return slide_doc.image_src
return None
def is_continue_slide_required(first_slide):
# check if first slide itself is not completed
if not first_slide.is_completed:
return False
# check if there is any active slide which is not completed
return frappe.db.exists('Onboarding Slide', {
'is_completed': 0,
'slide_order': ('!=', 0),
'slide_type': ('!=', 'Continue')
})
@frappe.whitelist()
def create_onboarding_docs(values, doctype=None, app=None, slide_type=None):
data = json.loads(values)
doc = frappe.new_doc(doctype)
if hasattr(doc, 'create_onboarding_docs'):
doc.create_onboarding_docs(data)
else:
create_generic_onboarding_doc(data, doctype, slide_type)
def create_generic_onboarding_doc(data, doctype, slide_type):
if slide_type == 'Settings':
doc = frappe.get_single(doctype)
for entry in data:
doc.set(entry, data.get(entry))
doc.save()
elif slide_type == 'Create':
doc = frappe.new_doc(doctype)
for entry in data:
doc.set(entry, data.get(entry))
doc.flags.ignore_mandatory = True
doc.flags.ignore_links = True
doc.insert()
@frappe.whitelist()
def mark_slide_as_completed(slide_title):
frappe.db.set_value('Onboarding Slide', slide_title, 'is_completed', 1)
def get_first_slide():
slides = frappe.db.get_all('Onboarding Slide',
filters={'slide_order': ('!=', 0), 'slide_type': ('!=', 'Continue')},
order_by='slide_order',
fields=['name', 'is_completed']
)
return slides[0]

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
import unittest
class TestSetupWizardSlide(unittest.TestCase):
class TestOnboardingSlide(unittest.TestCase):
pass

View file

@ -33,12 +33,6 @@
"in_list_view": 1,
"label": "Fieldname"
},
{
"fieldname": "options",
"fieldtype": "Text",
"in_list_view": 1,
"label": "Options"
},
{
"fieldname": "align",
"fieldtype": "Select",
@ -60,13 +54,19 @@
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "options",
"fieldtype": "Text",
"in_list_view": 1,
"label": "Options"
}
],
"istable": 1,
"modified": "2019-11-25 16:50:53.994656",
"modified": "2019-12-02 16:43:51.930018",
"modified_by": "Administrator",
"module": "Desk",
"name": "Setup Wizard Slide Field",
"name": "Onboarding Slide Field",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class SetupWizardHelpLink(Document):
class OnboardingSlideField(Document):
pass

View file

@ -25,7 +25,7 @@
"modified": "2019-11-19 13:39:57.716248",
"modified_by": "Administrator",
"module": "Desk",
"name": "Setup Wizard Help Link",
"name": "Onboarding Slide Help Link",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class SetupWizardSlideField(Document):
class OnboardingSlideHelpLink(Document):
pass

View file

@ -1,97 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
class SetupWizardSlide(Document):
def on_update(self):
if self.ref_doctype:
module = frappe.db.get_value('DocType', self.ref_doctype, 'module')
else:
module = self.slide_module
export_to_files(record_list=[['Setup Wizard Slide', self.name]], record_module=module)
def get_onboarding_slides_as_list():
slides = []
slide_docs = frappe.get_all('Setup Wizard Slide',
filters={'slide_order': ('!=', 0)},
order_by='slide_order')
for entry in slide_docs:
# using get_doc because child table fields are not fetched in get_all
slide_doc = frappe.get_doc('Setup Wizard Slide', entry.name)
if frappe.scrub(slide_doc.app) in frappe.get_installed_apps():
slides.append(frappe._dict(
slide_type=slide_doc.slide_type,
title=slide_doc.slide_title,
help=slide_doc.slide_desc,
fields=slide_doc.slide_fields,
help_links=get_help_links(slide_doc),
add_more=slide_doc.add_more_button,
max_count=slide_doc.max_count,
submit_method=slide_doc.submit_method,
image_src=get_slide_image(slide_doc),
ref_doctype=slide_doc.ref_doctype,
app=slide_doc.app
))
return slides
@frappe.whitelist()
def get_onboarding_slides():
slides = []
slide_list = get_onboarding_slides_as_list()
active_domains = frappe.get_active_domains()
for slide in slide_list:
if not slide.domains or any(domain in active_domains for domain in slide.domains):
slides.append(slide)
return slides
def get_help_links(slide_doc):
links=[]
for link in slide_doc.help_links:
links.append({
'label': link.label,
'video_id': link.video_id
})
return links
def get_slide_image(slide_doc):
if slide_doc.image_src:
return slide_doc.image_src
return None
@frappe.whitelist()
def create_onboarding_docs(values, doctype=None, submit_method=None, app=None, slide_type=None):
data = json.loads(values)
if submit_method:
try:
method = frappe.scrub(app) + '.utilities.onboarding_utils.' + submit_method
frappe.call(method, data)
except AttributeError:
create_generic_onboarding_doc(data, doctype, slide_type)
else:
doc = frappe.new_doc(doctype)
if hasattr(doc, 'create_onboarding_docs'):
doc.create_onboarding_docs(data)
else:
create_generic_onboarding_doc(data, doctype, slide_type)
def create_generic_onboarding_doc(data, doctype, slide_type):
if slide_type == 'Settings':
doc = frappe.get_single(doctype)
for entry in data:
doc.set(entry, data.get(entry))
doc.save()
elif slide_type == 'Create':
doc = frappe.new_doc(doctype)
for entry in data:
doc.set(entry, data.get(entry))
doc.flags.ignore_mandatory = True
doc.flags.ignore_links = True
doc.insert()

View file

@ -105,7 +105,7 @@ def get_next(doctype, value, prev, filters, sort_order, sort_field):
res = frappe.get_list(doctype,
fields = ["name"],
filters = filters,
order_by = sort_field + " " + sort_order,
order_by = "`tab{0}`.{1}".format(doctype, sort_field) + " " + sort_order,
limit_start=0, limit_page_length=1, as_list=True)
if not res:

View file

@ -48,7 +48,7 @@ def get_group_by_count(doctype, current_filters, field):
else:
return frappe.db.get_list(doctype,
filters=current_filters,
group_by=field,
group_by='`tab{0}`.{1}'.format(doctype, field),
fields=['count(*) as count', '`{}` as name'.format(field)],
order_by='count desc',
limit=50,

View file

@ -19,6 +19,14 @@
background: #f0f4f7;
}
.from-date-field .clearfix{
display: none;
}
.from-date-field {
margin-left: 10px;
}
.select-time:focus, .select-doctype:focus, .select-filter:focus, .select-sort:focus {
background: #f0f4f7;
}

View file

@ -41,7 +41,11 @@ class Leaderboard {
return field;
});
}
this.timespans = ["Week", "Month", "Quarter", "Year", "All Time"];
this.timespans = [
"This Week", "This Month", "This Quarter", "This Year",
"Last Week", "Last Month", "Last Quarter", "Last Year",
"All Time", "Select From Date"
];
// for saving current selected filters
const _initial_doctype = frappe.get_route()[1] || this.doctypes[0];
@ -103,7 +107,8 @@ class Leaderboard {
this.timespans.map(d => {
return {"label": __(d), value: d };
})
);
);
this.create_from_date_field();
this.type_select = this.page.add_select(__("Field"),
this.options.selected_filter.map(d => {
@ -113,7 +118,12 @@ class Leaderboard {
this.timespan_select.on("change", (e) => {
this.options.selected_timespan = e.currentTarget.value;
this.make_request();
if (this.options.selected_timespan === 'Select From Date') {
this.from_date_field.show();
} else {
this.from_date_field.hide();
this.make_request();
}
});
this.type_select.on("change", (e) => {
@ -122,6 +132,28 @@ class Leaderboard {
});
}
create_from_date_field() {
let timespan_field = $(this.parent).find(`.frappe-control[data-original-title='Timespan']`);
this.from_date_field = $(`<div class="from-date-field"></div>`).insertAfter(timespan_field).hide();
let date_field = frappe.ui.form.make_control({
df: {
fieldtype: 'Date',
fieldname: 'selected_from_date',
placeholder: frappe.datetime.month_start(),
default: frappe.datetime.month_start(),
input_class: 'input-sm',
reqd: 1,
change: () => {
this.selected_from_date = date_field.get_value();
if (this.selected_from_date) this.make_request();
}
},
parent: $(this.parent).find('.from-date-field'),
render_input: 1
});
}
render_selected_doctype() {
this.$sidebar_list.on("click", "li", (e)=> {
@ -207,7 +239,6 @@ class Leaderboard {
this.leaderboard_config[this.options.selected_doctype].method,
{
'from_date': this.get_from_date(),
'timespan': this.options.selected_timespan,
'company': this.options.selected_company,
'field': this.options.selected_filter_item,
'limit': this.leaderboard_limit,
@ -360,17 +391,20 @@ class Leaderboard {
get_from_date() {
let timespan = this.options.selected_timespan.toLowerCase();
let current_date = frappe.datetime.now_date();
let date = '';
if (timespan === "month") {
date = frappe.datetime.add_months(current_date, -1);
} else if (timespan === "quarter") {
date = frappe.datetime.add_months(current_date, -3);
} else if (timespan === "year") {
date = frappe.datetime.add_months(current_date, -12);
} else if (timespan === "week") {
date = frappe.datetime.add_days(current_date, -7);
let get_from_date = {
"this week": frappe.datetime.week_start(),
"this month": frappe.datetime.month_start(),
"this quarter": frappe.datetime.quarter_start(),
"this year": frappe.datetime.year_start(),
"last week": frappe.datetime.add_days(current_date, -7),
"last month": frappe.datetime.add_months(current_date, -1),
"last quarter": frappe.datetime.add_months(current_date, -3),
"last year": frappe.datetime.add_months(current_date, -12),
"all time": "",
"select from date": this.selected_from_date || frappe.datetime.month_start()
}
return date;
return get_from_date[timespan];
}
}

View file

@ -50,7 +50,7 @@ def sanitize_searchfield(searchfield):
# this is called by the Link Field
@frappe.whitelist()
def search_link(doctype, txt, query=None, filters=None, page_length=20, searchfield=None, reference_doctype=None, ignore_user_permissions=False):
search_widget(doctype, txt, query, searchfield=searchfield, page_length=page_length, filters=filters, reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions)
search_widget(doctype, txt.strip(), query, searchfield=searchfield, page_length=page_length, filters=filters, reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions)
frappe.response['results'] = build_for_autosuggest(frappe.response["values"])
del frappe.response["values"]

View file

@ -456,9 +456,9 @@ class Email:
def show_attached_email_headers_in_content(self, part):
# get the multipart/alternative message
try:
from html import escape # python 3.x
from html import escape # python 3.x
except ImportError:
from cgi import escape # python 2.x
from cgi import escape # python 2.x
message = list(part.walk())[1]
headers = []
@ -480,7 +480,7 @@ class Email:
"""Detect chartset."""
charset = part.get_content_charset()
if not charset:
charset = chardet.detect(frappe.safe_encode(part))['encoding']
charset = chardet.detect(cstr(part))['encoding']
return charset
@ -514,7 +514,7 @@ class Email:
'fcontent': fcontent,
})
cid = (part.get("Content-Id") or "").strip("><")
cid = (cstr(part.get("Content-Id")) or "").strip("><")
if cid:
self.cid_map[fname] = cid

View file

@ -7,10 +7,5 @@ frappe.ui.form.on('Currency', {
if(!frm.doc.enabled) {
frm.set_intro(__("This Currency is disabled. Enable to use in transactions"));
}
},
after_save(frm) {
if (frm.doc.enabled)
locals[':Currency'][frm.doc.name] = Object.assign(frm.doc, { doctype: ':Currency' });
}
});

View file

@ -21,13 +21,13 @@ from frappe.database import setup_database
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
def install_db(root_login="root", root_password=None, db_name=None, source_sql=None,
admin_password=None, verbose=True, force=0, site_config=None, reinstall=False,
db_type=None):
admin_password=None, verbose=True, force=0, site_config=None, reinstall=False,
db_type=None, db_host=None, db_port=None):
if not db_type:
db_type = frappe.conf.db_type or 'mariadb'
make_conf(db_name, site_config=site_config, db_type=db_type)
make_conf(db_name, site_config=site_config, db_type=db_type, db_host=db_host, db_port=db_port)
frappe.flags.in_install_db = True
frappe.flags.root_login = root_login
@ -191,14 +191,14 @@ def init_singles():
doc.flags.ignore_validate=True
doc.save()
def make_conf(db_name=None, db_password=None, site_config=None, db_type=None):
def make_conf(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None):
site = frappe.local.site
make_site_config(db_name, db_password, site_config, db_type=db_type)
make_site_config(db_name, db_password, site_config, db_type=db_type, db_host=db_host, db_port=db_port)
sites_path = frappe.local.sites_path
frappe.destroy()
frappe.init(site, sites_path=sites_path)
def make_site_config(db_name=None, db_password=None, site_config=None, db_type=None):
def make_site_config(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None):
frappe.create_folder(os.path.join(frappe.local.site_path))
site_file = get_site_config_path()
@ -209,6 +209,12 @@ def make_site_config(db_name=None, db_password=None, site_config=None, db_type=N
if db_type:
site_config['db_type'] = db_type
if db_host:
site_config['db_host'] = db_host
if db_port:
site_config['db_port'] = db_port
with open(site_file, "w") as f:
f.write(json.dumps(site_config, indent=1, sort_keys=True))

View file

@ -501,6 +501,10 @@ class DatabaseQuery(object):
value = f.value or "''"
fallback = "''"
elif f.fieldname == 'name':
value = f.value or "''"
fallback = "''"
else:
value = flt(f.value)
fallback = 0

View file

@ -44,9 +44,9 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe
("data_migration", "data_migration_mapping"),
("data_migration", "data_migration_plan_mapping"),
("data_migration", "data_migration_plan"),
("desk", "setup_wizard_slide_field"),
("desk", "setup_wizard_help_link"),
("desk", "setup_wizard_slide")):
("desk", "onboarding_slide_field"),
("desk", "onboarding_slide_help_link"),
("desk", "onboarding_slide")):
files.append(os.path.join(frappe.get_app_path("frappe"), d[0],
"doctype", d[1], d[1] + ".json"))
@ -75,7 +75,7 @@ def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=F
# load in sequence - warning for devs
document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format',
'website_theme', 'web_form', 'notification', 'print_style',
'data_migration_mapping', 'data_migration_plan', 'setup_wizard_slide']
'data_migration_mapping', 'data_migration_plan', 'onboarding_slide']
for doctype in document_types:
doctype_path = os.path.join(start_path, doctype)
if os.path.exists(doctype_path):

View file

@ -6,11 +6,11 @@ frappe.patches.v8_0.update_global_search_table
frappe.patches.v7_0.update_auth
frappe.patches.v8_0.drop_in_dialog #2017-09-22
frappe.patches.v7_2.remove_in_filter
frappe.patches.v11_0.drop_column_apply_user_permissions
execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22
execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20
execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23
execute:frappe.reload_doc('core', 'doctype', 'doctype_link', force=True) #2019-09-23
execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22
execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20
frappe.patches.v11_0.drop_column_apply_user_permissions
execute:frappe.reload_doc('core', 'doctype', 'custom_docperm')
execute:frappe.reload_doc('core', 'doctype', 'docperm') #2018-05-29
execute:frappe.reload_doc('core', 'doctype', 'comment')

View file

@ -3,6 +3,9 @@ import frappe
def execute():
if frappe.db.count("File", filters={"attached_to_doctype": "Prepared Report", "is_private": 0}) > 10000:
frappe.db.auto_commit_on_many_writes = True
files = frappe.get_all("File", fields=["name", "attached_to_name"], filters={"attached_to_doctype": "Prepared Report", "is_private": 0})
for file_dict in files:
# For some reason Prepared Report doc might not exist, check if it exists first
@ -17,3 +20,7 @@ def execute():
else:
# If Prepared Report doc doesn't exist then the file doc is useless. Delete it.
frappe.delete_doc("File", file_dict.name)
if frappe.db.auto_commit_on_many_writes:
frappe.db.auto_commit_on_many_writes = False

View file

@ -3,23 +3,23 @@
"app": "ERPNext",
"creation": "2019-11-22 13:25:42.892593",
"docstatus": 0,
"doctype": "Setup Wizard Slide",
"doctype": "Onboarding Slide",
"domains": [],
"help_links": [
{
"label": "Know more about printing and branding through letterhead",
"label": "Need Help?",
"video_id": "cKZHcx1znMc"
}
],
"idx": 0,
"image_src": "/assets/erpnext/images/illustrations/letterhead.png",
"image_src": "/assets/erpnext/images/illustrations/letterhead-onboard.png",
"max_count": 0,
"modified": "2019-11-27 11:39:56.213373",
"modified": "2019-12-03 22:54:57.618989",
"modified_by": "Administrator",
"name": "Company Letter Head",
"owner": "Administrator",
"ref_doctype": "Letter Head",
"slide_desc": "Attach Letterhead: (Keep it web friendly as 1024px by 128px)",
"slide_desc": "<p>The letter head will appear across all print formats and PDFs</p>\n<p class=\"text-muted\">Keep it web friendly as 1024px by 128px</p>",
"slide_fields": [
{
"align": "center",
@ -32,6 +32,5 @@
],
"slide_order": 20,
"slide_title": "Company Letter Head",
"slide_type": "Create",
"submit_method": ""
"slide_type": "Create"
}

View file

@ -486,22 +486,14 @@ frappe.Application = Class.extend({
},
setup_onboarding_wizard: () => {
var me = this;
frappe.call('frappe.desk.doctype.setup_wizard_slide.setup_wizard_slide.get_onboarding_slides').then(res => {
frappe.call('frappe.desk.doctype.onboarding_slide.onboarding_slide.get_onboarding_slides').then(res => {
if (res.message) {
let slides = res.message;
if (slides.length) {
frappe.require("assets/frappe/js/frappe/ui/onboarding_dialog.js", () => {
me.progress_dialog = new frappe.setup.OnboardingDialog({
slides: slides
});
me.progress_dialog.show();
frappe.call({
method: "frappe.desk.page.setup_wizard.setup_wizard.reset_is_first_startup",
args: {},
callback: () => {}
});
this.progress_dialog = new frappe.setup.OnboardingDialog({
slides: slides
});
this.progress_dialog.show();
}
}
});

View file

@ -10,13 +10,7 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({
set_options() {
if (this.df.options) {
let options = this.df.options || [];
if (typeof options === 'string') {
options = options.split('\n');
}
if (typeof options[0] === 'string') {
options = options.map(o => ({ label: o, value: o }));
}
this._data = options;
this._data = this.parse_options(options);
}
},
@ -100,6 +94,9 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({
},
validate(value) {
if (this.df.ignore_validation) {
return value || '';
}
let valid_values = this.awesomplete._list.map(d => d.value);
if (!valid_values.length) {
return value;
@ -111,11 +108,22 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({
}
},
parse_options(options) {
if (typeof options === 'string') {
options = options.split('\n');
}
if (typeof options[0] === 'string') {
options = options.map(o => ({ label: o, value: o }));
}
return options;
},
get_data() {
return this._data || [];
},
set_data(data) {
data = this.parse_options(data);
if (this.awesomplete) {
this.awesomplete.list = data;
}

View file

@ -4,11 +4,6 @@ frappe.ui.form.ControlCurrency = frappe.ui.form.ControlFloat.extend({
return isNaN(parseFloat(value)) ? "" : formatted_value;
},
get_number_format: function() {
var currency = frappe.meta.get_field_currency(this.df, this.get_doc());
return get_number_format(currency);
},
get_precision: function() {
// always round based on field precision or currency's precision
// this method is also called in this.parse()

View file

@ -1,10 +1,7 @@
frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({
parse: function(value) {
value = this.eval_expression(value);
return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision(),
// While parsing currency, get_number_format passes currency's number_format
// In case of parsing float, it passes global number_format
this.get_number_format());
return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision());
},
format_for_input: function(value) {
@ -17,8 +14,8 @@ frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({
},
get_number_format: function() {
// In case of 'Float' field currency's number_format shouldn't be used for formatting
return get_number_format();
var currency = frappe.meta.get_field_currency(this.df, this.get_doc());
return get_number_format(currency);
},
get_precision: function() {

View file

@ -81,6 +81,7 @@ frappe.ui.form.Review = class Review {
label: __('To User'),
reqd: 1,
options: user_options,
ignore_validation: 1,
description: __('Only users involved in the document are listed')
}, {
fieldname: 'review_type',

View file

@ -22,11 +22,21 @@ frappe.setup.OnboardingSlide = class OnboardingSlide extends frappe.ui.Slide {
}
}
setup_form() {
super.setup_form();
const fields = this.get_atomic_fields();
if (fields.length == 1) {
this.$form_wrapper.addClass("text-center");
} else {
this.$form_wrapper.removeClass("text-center");
}
}
before_show() {
(this.id === 0) ?
this.$next_btn.text(__('Start')) : this.$next_btn.text(__('Next'));
this.$next_btn.text(__('Let\'s Start')) : this.$next_btn.text(__('Next'));
//last slide
if (this.id === this.parent[0].children.length-1) {
if (this.is_last_slide()) {
this.$complete_btn.removeClass('hide').addClass('action primary');
this.$next_btn.removeClass('action primary');
this.$action_button = this.$complete_btn;
@ -35,33 +45,27 @@ frappe.setup.OnboardingSlide = class OnboardingSlide extends frappe.ui.Slide {
}
primary_action() {
let me = this;
if (this.set_values()) {
this.$action_button.addClass('disabled');
if (me.add_more) me.values.max_count = me.max_count;
frappe.call({
method: 'frappe.desk.doctype.setup_wizard_slide.setup_wizard_slide.create_onboarding_docs',
args: {
values: me.values,
doctype: me.ref_doctype,
submit_method: me.submit_method,
app: me.app,
slide_type: me.slide_type
},
callback: function() {
if (me.id === me.parent[0].children.length-1) {
$('.onboarding-dialog').modal('toggle');
frappe.msgprint({
message: __('You are all set up!'),
indicator: 'green',
title: __('Success')
});
}
},
onerror: function() {
me.slides_footer.find('.primary').removeClass('disabled');
},
freeze: true
const primary_method = 'frappe.desk.doctype.onboarding_slide.onboarding_slide.create_onboarding_docs';
if (this.add_more) {
this.values.max_count = this.max_count;
}
frappe.call(primary_method, {
values: this.values,
doctype: this.ref_doctype,
app: this.app,
slide_type: this.slide_type
}).then(() => {
if (this.is_last_slide()) {
this.reset_is_first_startup();
$('.onboarding-dialog').modal('toggle');
frappe.msgprint({
message: __('You are all set up!'),
indicator: 'green',
title: __('Success')
});
}
});
}
}
@ -74,10 +78,7 @@ frappe.setup.OnboardingSlide = class OnboardingSlide extends frappe.ui.Slide {
setup_help_links() {
this.help_links.map(link => {
let $link = $(
`<a target="_blank" class="small text-muted">${link.label}</a>
<span class="small">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</span>`
`<a target="_blank" class="small text-muted">${link.label || __("Need Help?")}</a>`
);
if (link.video_id) {
$link.on('click', () => {
@ -89,11 +90,34 @@ frappe.setup.OnboardingSlide = class OnboardingSlide extends frappe.ui.Slide {
}
setup_action_button() {
if (this.slide_type !== 'Information') {
if (this.slide_type === 'Create' || this.slide_type == 'Settings' || this.is_last_slide()) {
this.$action_button.addClass('primary');
} else {
this.$action_button.removeClass('primary');
}
this.$action_button.on('click', () => {
if (this.slide_type != 'Continue') {
this.mark_as_completed();
}
});
}
mark_as_completed() {
frappe.call({
method: 'frappe.desk.doctype.onboarding_slide.onboarding_slide.mark_slide_as_completed',
args: {slide_title: this.title},
callback: () => {},
freeze: true
});
}
reset_is_first_startup() {
frappe.call({
method: "frappe.desk.page.setup_wizard.setup_wizard.reset_is_first_startup",
args: {},
callback: () => {}
});
}
};
@ -109,7 +133,6 @@ frappe.setup.OnboardingDialog = class OnboardingDialog {
this.dialog = new frappe.ui.Dialog({
static: true,
minimizable: false,
title: __("Let's Onboard!")
});
this.$wrapper = $(this.dialog.$wrapper).addClass('onboarding-dialog');
this.slide_container = new frappe.ui.Slides({
@ -126,11 +149,7 @@ frappe.setup.OnboardingDialog = class OnboardingDialog {
}
});
this.$wrapper.find('.modal-title').prepend(
`<span class="onboarding-icon">
<i class="fa fa-rocket" aria-hidden="true"></i>
</span>`
);
this.$wrapper.find('.modal-header').remove();
}
show() {

View file

@ -27,7 +27,7 @@ frappe.ui.Slide = class Slide {
<div class="form"></div>
<div class="add-more text-center" style="margin-top: 5px;">
<a class="form-more-btn hide btn btn-default btn-xs">
<span><i class="fa fa-plus small" aria-hidden="true"></i></span>
<span>Add More</span>
</a>
</div>
</div>
@ -36,6 +36,7 @@ frappe.ui.Slide = class Slide {
this.$content = this.$body.find(".content");
this.$form = this.$body.find(".form");
this.$primary_btn = this.slides_footer.find('.primary');
this.$form_wrapper = this.$body.find(".form-wrapper");
if(this.image_src) this.$content.append(
$(`<img src="${this.image_src}" style="margin: 20px;">`));
@ -79,14 +80,14 @@ frappe.ui.Slide = class Slide {
if(this.add_more) {
this.count = 1;
fields = fields.map((field, i) => {
if(field.fieldname) {
if (field.fieldname) {
field.fieldname += '_1';
}
if(i === 1 && this.mandatory_entry) {
if (i === 1 && this.mandatory_entry) {
field.reqd = 1;
}
if(!field.static) {
if(field.label) field.label += ' 1';
if (!field.static) {
if (field.label) field.label;
}
return field;
});
@ -106,10 +107,10 @@ frappe.ui.Slide = class Slide {
set_values() {
this.values = this.form.get_values();
if(this.values===null) {
if (this.values===null) {
return false;
}
if(this.validate && !this.validate()) {
if (this.validate && !this.validate()) {
return false;
}
return true;
@ -121,14 +122,16 @@ frappe.ui.Slide = class Slide {
.on('click', () => {
this.count++;
var fields = JSON.parse(JSON.stringify(this.fields));
this.form.add_fields(fields.map(field => {
if(field.fieldname) field.fieldname += '_' + this.count;
if(!field.static) {
if(field.label) field.label += ' ' + this.count;
if (field.fieldname) field.fieldname += '_' + this.count;
if (!field.static) {
if (field.label) field.label;
}
field.reqd = 0;
return field;
}));
if(this.count === this.max_count) {
this.$more.addClass('hide');
}
@ -156,7 +159,7 @@ frappe.ui.Slide = class Slide {
var empty_fields = this.reqd_fields.filter((field) => {
return !field.get_value();
});
if(empty_fields.length) {
if (empty_fields.length) {
this.slides_footer.find('.action').addClass('disabled');
} else {
this.slides_footer.find('.action').removeClass('disabled');
@ -173,6 +176,13 @@ frappe.ui.Slide = class Slide {
});
}
is_last_slide() {
if (this.id === this.parent[0].children.length-1) {
return true;
}
return false;
}
before_show() { }
show_slide() {

View file

@ -90,6 +90,14 @@ $.extend(frappe.datetime, {
return moment().endOf("month").format();
},
quarter_start: function() {
return moment().startOf("quarter").format();
},
quarter_end: function() {
return moment().endOf("quarter").format();
},
year_start: function(){
return moment().startOf("year").format();
},

View file

@ -144,10 +144,7 @@ function get_currency_symbol(currency) {
}
function get_number_format(currency) {
let format = null;
if (currency) format = frappe.model.get_value(":Currency", currency, "number_format");
return format || (frappe.boot && frappe.boot.sysdefaults && frappe.boot.sysdefaults.number_format) || "#,###.##";
return (frappe.boot && frappe.boot.sysdefaults && frappe.boot.sysdefaults.number_format) || "#,###.##";
}
function get_number_format_info(format) {

View file

@ -308,6 +308,7 @@ frappe.views.Calendar = Class.extend({
doctype: this.doctype,
start: this.get_system_datetime(start),
end: this.get_system_datetime(end),
fields: this.fields,
filters: this.list_view.filter_area.get(),
field_map: this.field_map
};
@ -356,9 +357,13 @@ frappe.views.Calendar = Class.extend({
prepare_colors: function(d) {
let color, color_name;
if(this.get_css_class) {
color_name = this.color_map[this.get_css_class(d)];
color_name = frappe.ui.color.validate_hex(color_name) ?
color_name : 'blue';
color_name = this.color_map[this.get_css_class(d)] || 'blue';
if (color_name.startsWith("#")) {
color_name = frappe.ui.color.validate_hex(color_name) ?
color_name : 'blue';
}
d.backgroundColor = frappe.ui.color.get(color_name, 'extra-light');
d.textColor = frappe.ui.color.get(color_name, 'dark');
} else {

View file

@ -507,6 +507,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
})
};
}
options.axisOptions = {
shortenYAxisNumbers: 1
};
return options;
}
@ -561,7 +564,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
get_possible_chart_options() {
const columns = this.columns;
const rows = this.raw_data.result.filter(value => Object.keys(value).length);
const first_row = Array.isArray(rows[0]) ? rows[0] : Object.values(rows[0]);
const first_row = Array.isArray(rows[0]) ? rows[0] : columns.map(col => rows[0][col.fieldname]);
const me = this
const indices = first_row.reduce((accumulator, current_value, current_index) => {

View file

@ -28,6 +28,10 @@ p {
margin: 10px 0;
}
details > summary {
cursor: pointer;
}
.text-color {
color: @text-color !important;
}

View file

@ -897,6 +897,7 @@ input[type="checkbox"] {
&:focus {
outline: none;
}
.fa-circle {
font-size: 10px;
margin: 0px 2px;
@ -928,6 +929,7 @@ input[type="checkbox"] {
}
.lead {
margin-top: 20px;
font-weight: 500;
}
.success-state {
margin-bottom: 20px;
@ -962,6 +964,12 @@ input[type="checkbox"] {
// Onboarding Dialog
.onboarding-dialog {
.slide-body {
width: 65%;
margin-right: auto;
margin-left: auto;
}
.modal-dialog {
width: 50%;
height: 80%;
@ -969,7 +977,7 @@ input[type="checkbox"] {
}
.onboarding-icon {
color: #3246F5;
color: @text-muted;
margin-right: 5px;
}
@ -980,8 +988,8 @@ input[type="checkbox"] {
}
img {
max-width: 128px;
max-height: 128px;
max-height: 175px;
width: auto;
}
.slides-progress {

View file

@ -239,6 +239,18 @@
.progress-area {
padding-top: 15px;
padding-bottom: 15px;
.progress-chart {
padding-top: 15px;
}
.progress {
margin-bottom: 5px;
}
.progress-message {
margin-top: 0px;
}
}
.form-links {

View file

@ -10,7 +10,8 @@
// To compensate for percieved centering
.null-state {
height: 12em !important;
height: 15rem !important;
max-height: 150px;
width: auto;
}
@ -616,4 +617,4 @@ input.list-check-all, input.list-row-checkbox {
.file-title {
margin-top: 5px;
}
}

View file

@ -80,7 +80,7 @@ class TestGlobalSearch(unittest.TestCase):
make_property_setter(doctype, "repeat_on", "in_global_search", 1, "Int")
global_search.rebuild_for_doctype(doctype)
results = global_search.search('Monthly')
self.assertEqual(len(results), 2)
self.assertEqual(len(results), 3)
def test_delete_doc(self):
self.insert_test_events()

View file

@ -6,7 +6,7 @@ import unittest
import frappe
import json
from frappe.desk.listview import get_list_settings, set_list_settings
from frappe.desk.listview import get_list_settings, set_list_settings, get_group_by_count
class TestListView(unittest.TestCase):
def setUp(self):
@ -51,3 +51,13 @@ class TestListView(unittest.TestCase):
self.assertEqual(settings.disable_count, 0)
self.assertEqual(settings.disable_sidebar_stats, 0)
def test_list_view_child_table_filter_with_created_by_filter(self):
if frappe.db.exists("Note", "Test created by filter with child table filter"):
frappe.delete_doc("Note", "Test created by filter with child table filter")
doc = frappe.get_doc({"doctype": "Note", "title": "Test created by filter with child table filter", "public": 1})
doc.append("seen_by", {"user": "Administrator"})
doc.insert()
data = {d.name: d.count for d in get_group_by_count('Note', '[["Note Seen By","user","=","Administrator"]]', 'owner')}
self.assertEqual(data['Administrator'], 1)

View file

@ -91,3 +91,20 @@ def create_doctype(name, fields):
}],
"name": name
}).insert()
def create_contact_records():
if frappe.db.get_all('Contact', {'first_name': 'Test Form Contact 1'}):
return
insert_contact('Test Form Contact 1', '12345')
insert_contact('Test Form Contact 2', '54321')
insert_contact('Test Form Contact 3', '12345')
def insert_contact(first_name, phone_number):
doc = frappe.get_doc({
'doctype': 'Contact',
'first_name': first_name
})
doc.append('phone_nos', {'phone': phone_number})
doc.insert()

View file

@ -418,25 +418,31 @@ def search(text, start=0, limit=20, doctype=""):
from frappe.desk.doctype.global_search_settings.global_search_settings import get_doctypes_for_global_search
results = []
texts = [t.strip() for t in text.split('&') if t]
priorities = get_doctypes_for_global_search()
allowed_doctypes = ",".join(["'{0}'".format(dt) for dt in priorities])
for text in texts:
mariadb_conditions = ''
postgres_conditions = ''
offset = ''
sorted_results = []
if doctype:
mariadb_conditions = postgres_conditions = '`doctype` = {} AND '.format(frappe.db.escape(doctype))
allowed_doctypes = get_doctypes_for_global_search()
for text in set(text.split('&')):
text = text.strip()
if not text:
continue
conditions = '1=1'
offset = ''
mariadb_text = frappe.db.escape('+' + text + '*')
mariadb_fields = '`doctype`, `name`, `content`, MATCH (`content`) AGAINST ({} IN BOOLEAN MODE) AS rank'.format(mariadb_text)
postgres_fields = '`doctype`, `name`, `content`, TO_TSVECTOR("content") @@ PLAINTO_TSQUERY({}) AS rank'.format(frappe.db.escape(text))
if allowed_doctypes:
mariadb_conditions += '`doctype` IN ({})'.format(allowed_doctypes)
postgres_conditions += '`doctype` IN ({})'.format(allowed_doctypes)
values = {}
if doctype:
conditions = '`doctype` = %(doctype)s'
values['doctype'] = doctype
elif allowed_doctypes:
conditions = '`doctype` IN %(allowed_doctypes)s'
values['allowed_doctypes'] = tuple(allowed_doctypes)
if int(start) > 0:
offset = 'OFFSET {}'.format(start)
@ -451,41 +457,27 @@ def search(text, start=0, limit=20, doctype=""):
"""
result = frappe.db.multisql({
'mariadb': common_query.format(fields=mariadb_fields, conditions=mariadb_conditions, limit=limit, offset=offset),
'postgres': common_query.format(fields=postgres_fields, conditions=postgres_conditions, limit=limit, offset=offset)
}, as_dict=True)
'mariadb': common_query.format(fields=mariadb_fields, conditions=conditions, limit=limit, offset=offset),
'postgres': common_query.format(fields=postgres_fields, conditions=conditions, limit=limit, offset=offset)
}, values=values, as_dict=True)
tmp_result=[]
for i in result:
if i.rank > 0.0:
if i in results or not results:
tmp_result.extend([i])
results.extend(tmp_result)
for r in results:
try:
if frappe.get_meta(r.doctype).image_field:
r.image = frappe.db.get_value(r.doctype, r.name, frappe.get_meta(r.doctype).image_field)
except Exception:
frappe.clear_messages()
sorted_results = []
for priority in priorities:
tmp_result = []
if not results:
break
results.extend(result)
# sort results based on allowed_doctype's priority
for doctype in allowed_doctypes:
for index, r in enumerate(results):
if r.doctype == priority:
tmp_result.extend([r])
results.pop(index)
if r.doctype == doctype and r.rank > 0.0:
try:
meta = frappe.get_meta(r.doctype)
if meta.image_field:
r.image = frappe.db.get_value(r.doctype, r.name, meta.image_field)
except Exception:
frappe.clear_messages()
sorted_results.extend(tmp_result)
sorted_results.extend([r])
return sorted_results
@frappe.whitelist(allow_guest=True)
def web_search(text, scope=None, start=0, limit=20):
"""

View file

@ -283,6 +283,12 @@ def update_oauth_user(user, data, provider):
if save:
user.flags.ignore_permissions = True
user.flags.no_welcome_mail = True
# set default signup role as per Portal Settings
default_role = frappe.db.get_single_value("Portal Settings", "default_role")
if default_role:
user.add_roles(default_role)
user.save()
def get_first_name(data):

View file

@ -210,7 +210,7 @@ def send_private_file(path):
blacklist = ['.svg', '.html', '.htm', '.xml']
if extension.lower() in blacklist:
response.headers.add(b'Content-Disposition', b'attachment', filename=filename.encode("utf-8"))
response.headers.add('Content-Disposition', 'attachment', filename=filename.encode("utf-8"))
response.mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'

View file

@ -14,6 +14,7 @@ import frappe, os, time
import schedule
from frappe.utils import now_datetime, get_datetime
from frappe.utils import get_sites
from frappe.installer import update_site_config
from frappe.core.doctype.user.user import STANDARD_USERS
from frappe.utils.background_jobs import get_jobs