diff --git a/cypress/integration/form.js b/cypress/integration/form.js index b7ddd6ecb7..5a8b85d19e 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -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(); + }); }); diff --git a/frappe/commands/site.py b/frappe/commands/site.py index e28fd36346..89e9ab7f34 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -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: diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index 21ed88addb..75fd0ad8c6 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -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()) diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 361030d07a..1848136bee 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -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): diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index ee139000e1..041b8c3011 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -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"]) diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json index 1aafdb47b0..e2ec921679 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json @@ -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", diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index 38123cea33..5d5bd35eec 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -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') diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 7de2bb20e5..35495954b4 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -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) \ No newline at end of file + frappe.throw(frappe._("Not Permitted"), frappe.PermissionError) diff --git a/frappe/core/page/background_jobs/background_jobs.html b/frappe/core/page/background_jobs/background_jobs.html index c5d598ccd3..08177ecf8a 100644 --- a/frappe/core/page/background_jobs/background_jobs.html +++ b/frappe/core/page/background_jobs/background_jobs.html @@ -11,7 +11,7 @@ {% for j in jobs %} - {{ j.queue.split(".").slice(-1)[0] }} + {{ j.queue.split(".").slice(-1)[0] }}
{{ frappe.utils.encode_tags(j.job_name) }} diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py index 10cb7b97ac..c8a2352968 100644 --- a/frappe/core/page/background_jobs/background_jobs.py +++ b/frappe/core/page/background_jobs/background_jobs.py @@ -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 diff --git a/frappe/database/db_manager.py b/frappe/database/db_manager.py index 0447f97273..80236b2dc2 100644 --- a/frappe/database/db_manager.py +++ b/frappe/database/db_manager.py @@ -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) diff --git a/frappe/desk/doctype/global_search_settings/global_search_settings.py b/frappe/desk/doctype/global_search_settings/global_search_settings.py index 0729fca5cb..85c9687ab3 100644 --- a/frappe/desk/doctype/global_search_settings/global_search_settings.py +++ b/frappe/desk/doctype/global_search_settings/global_search_settings.py @@ -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: diff --git a/frappe/desk/doctype/setup_wizard_help_link/__init__.py b/frappe/desk/doctype/onboarding_slide/__init__.py similarity index 100% rename from frappe/desk/doctype/setup_wizard_help_link/__init__.py rename to frappe/desk/doctype/onboarding_slide/__init__.py diff --git a/frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.js b/frappe/desk/doctype/onboarding_slide/onboarding_slide.js similarity index 73% rename from frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.js rename to frappe/desk/doctype/onboarding_slide/onboarding_slide.js index 7a802ad2b6..dc91f42913 100644 --- a/frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.js +++ b/frappe/desk/doctype/onboarding_slide/onboarding_slide.js @@ -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; diff --git a/frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.json b/frappe/desk/doctype/onboarding_slide/onboarding_slide.json similarity index 78% rename from frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.json rename to frappe/desk/doctype/onboarding_slide/onboarding_slide.json index 94d94851c3..3f6f0d719f 100644 --- a/frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.json +++ b/frappe/desk/doctype/onboarding_slide/onboarding_slide.json @@ -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": [ { diff --git a/frappe/desk/doctype/onboarding_slide/onboarding_slide.py b/frappe/desk/doctype/onboarding_slide/onboarding_slide.py new file mode 100644 index 0000000000..8c75d10b9a --- /dev/null +++ b/frappe/desk/doctype/onboarding_slide/onboarding_slide.py @@ -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 {0} 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] \ No newline at end of file diff --git a/frappe/desk/doctype/setup_wizard_slide/test_setup_wizard_slide.py b/frappe/desk/doctype/onboarding_slide/test_onboarding_slide.py similarity index 79% rename from frappe/desk/doctype/setup_wizard_slide/test_setup_wizard_slide.py rename to frappe/desk/doctype/onboarding_slide/test_onboarding_slide.py index 58652c4ec2..d78b9b6158 100644 --- a/frappe/desk/doctype/setup_wizard_slide/test_setup_wizard_slide.py +++ b/frappe/desk/doctype/onboarding_slide/test_onboarding_slide.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe import unittest -class TestSetupWizardSlide(unittest.TestCase): +class TestOnboardingSlide(unittest.TestCase): pass diff --git a/frappe/desk/doctype/setup_wizard_slide/__init__.py b/frappe/desk/doctype/onboarding_slide_field/__init__.py similarity index 100% rename from frappe/desk/doctype/setup_wizard_slide/__init__.py rename to frappe/desk/doctype/onboarding_slide_field/__init__.py diff --git a/frappe/desk/doctype/setup_wizard_slide_field/setup_wizard_slide_field.json b/frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.json similarity index 94% rename from frappe/desk/doctype/setup_wizard_slide_field/setup_wizard_slide_field.json rename to frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.json index 03d58d842b..0992aed092 100644 --- a/frappe/desk/doctype/setup_wizard_slide_field/setup_wizard_slide_field.json +++ b/frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.json @@ -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", diff --git a/frappe/desk/doctype/setup_wizard_help_link/setup_wizard_help_link.py b/frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.py similarity index 86% rename from frappe/desk/doctype/setup_wizard_help_link/setup_wizard_help_link.py rename to frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.py index fbb96bbd91..74b6782ff8 100644 --- a/frappe/desk/doctype/setup_wizard_help_link/setup_wizard_help_link.py +++ b/frappe/desk/doctype/onboarding_slide_field/onboarding_slide_field.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document -class SetupWizardHelpLink(Document): +class OnboardingSlideField(Document): pass diff --git a/frappe/desk/doctype/setup_wizard_slide_field/__init__.py b/frappe/desk/doctype/onboarding_slide_help_link/__init__.py similarity index 100% rename from frappe/desk/doctype/setup_wizard_slide_field/__init__.py rename to frappe/desk/doctype/onboarding_slide_help_link/__init__.py diff --git a/frappe/desk/doctype/setup_wizard_help_link/setup_wizard_help_link.json b/frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.json similarity index 94% rename from frappe/desk/doctype/setup_wizard_help_link/setup_wizard_help_link.json rename to frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.json index b97b482cac..a09ba50553 100644 --- a/frappe/desk/doctype/setup_wizard_help_link/setup_wizard_help_link.json +++ b/frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.json @@ -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, diff --git a/frappe/desk/doctype/setup_wizard_slide_field/setup_wizard_slide_field.py b/frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.py similarity index 85% rename from frappe/desk/doctype/setup_wizard_slide_field/setup_wizard_slide_field.py rename to frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.py index 1b880ef916..6ee5e4f7d7 100644 --- a/frappe/desk/doctype/setup_wizard_slide_field/setup_wizard_slide_field.py +++ b/frappe/desk/doctype/onboarding_slide_help_link/onboarding_slide_help_link.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document -class SetupWizardSlideField(Document): +class OnboardingSlideHelpLink(Document): pass diff --git a/frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.py b/frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.py deleted file mode 100644 index 9109d52a85..0000000000 --- a/frappe/desk/doctype/setup_wizard_slide/setup_wizard_slide.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index 6676bd1908..cf2a8f0879 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -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: diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py index fd0833eb51..e7f56d313e 100644 --- a/frappe/desk/listview.py +++ b/frappe/desk/listview.py @@ -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, diff --git a/frappe/desk/page/leaderboard/leaderboard.css b/frappe/desk/page/leaderboard/leaderboard.css index dbe9cca5b8..a3cb4d09c4 100644 --- a/frappe/desk/page/leaderboard/leaderboard.css +++ b/frappe/desk/page/leaderboard/leaderboard.css @@ -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; } diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js index 3e4c36add0..c64d2dcb4f 100644 --- a/frappe/desk/page/leaderboard/leaderboard.js +++ b/frappe/desk/page/leaderboard/leaderboard.js @@ -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 = $(`
`).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]; } } diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 7bcfe646ab..c70b650945 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -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"] diff --git a/frappe/email/receive.py b/frappe/email/receive.py index bccad1fec5..e5c8457b4e 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -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 diff --git a/frappe/geo/doctype/currency/currency.js b/frappe/geo/doctype/currency/currency.js index 1bc9865999..af2d6ebc4e 100644 --- a/frappe/geo/doctype/currency/currency.js +++ b/frappe/geo/doctype/currency/currency.js @@ -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' }); } }); diff --git a/frappe/installer.py b/frappe/installer.py index f691a6cb22..4b07ab8ce8 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -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)) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 64e242d6af..f9016d7fcf 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -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 diff --git a/frappe/model/sync.py b/frappe/model/sync.py index 989e1fbb99..b50ddb1160 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -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): diff --git a/frappe/patches.txt b/frappe/patches.txt index 603df8d778..90abc8c31d 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -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') diff --git a/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py b/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py index dd212d157e..f7b9e476a9 100644 --- a/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py +++ b/frappe/patches/v11_0/make_all_prepared_report_attachments_private.py @@ -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 + diff --git a/frappe/printing/setup_wizard_slide/company_letter_head/company_letter_head.json b/frappe/printing/onboarding_slide/company_letter_head/company_letter_head.json similarity index 61% rename from frappe/printing/setup_wizard_slide/company_letter_head/company_letter_head.json rename to frappe/printing/onboarding_slide/company_letter_head/company_letter_head.json index 3644a54089..2f51d2e18a 100644 --- a/frappe/printing/setup_wizard_slide/company_letter_head/company_letter_head.json +++ b/frappe/printing/onboarding_slide/company_letter_head/company_letter_head.json @@ -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": "

The letter head will appear across all print formats and PDFs

\n

Keep it web friendly as 1024px by 128px

", "slide_fields": [ { "align": "center", @@ -32,6 +32,5 @@ ], "slide_order": 20, "slide_title": "Company Letter Head", - "slide_type": "Create", - "submit_method": "" + "slide_type": "Create" } \ No newline at end of file diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 206094dd71..92194acdca 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -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(); } } }); diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js index 026cdbec25..0b31776caf 100644 --- a/frappe/public/js/frappe/form/controls/autocomplete.js +++ b/frappe/public/js/frappe/form/controls/autocomplete.js @@ -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; } diff --git a/frappe/public/js/frappe/form/controls/currency.js b/frappe/public/js/frappe/form/controls/currency.js index 4d6b37aede..5a50ad9de6 100644 --- a/frappe/public/js/frappe/form/controls/currency.js +++ b/frappe/public/js/frappe/form/controls/currency.js @@ -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() diff --git a/frappe/public/js/frappe/form/controls/float.js b/frappe/public/js/frappe/form/controls/float.js index 308d970f6e..027cfebc2a 100644 --- a/frappe/public/js/frappe/form/controls/float.js +++ b/frappe/public/js/frappe/form/controls/float.js @@ -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() { diff --git a/frappe/public/js/frappe/form/sidebar/review.js b/frappe/public/js/frappe/form/sidebar/review.js index f4b8069a6e..e187ca4693 100644 --- a/frappe/public/js/frappe/form/sidebar/review.js +++ b/frappe/public/js/frappe/form/sidebar/review.js @@ -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', diff --git a/frappe/public/js/frappe/ui/onboarding_dialog.js b/frappe/public/js/frappe/ui/onboarding_dialog.js index 1798d0fe68..962cf988e8 100644 --- a/frappe/public/js/frappe/ui/onboarding_dialog.js +++ b/frappe/public/js/frappe/ui/onboarding_dialog.js @@ -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 = $( - `${link.label} - - - ` + `${link.label || __("Need Help?")}` ); 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( - ` - - ` - ); + this.$wrapper.find('.modal-header').remove(); } show() { diff --git a/frappe/public/js/frappe/ui/slides.js b/frappe/public/js/frappe/ui/slides.js index d2e504337c..19a6cf9263 100644 --- a/frappe/public/js/frappe/ui/slides.js +++ b/frappe/public/js/frappe/ui/slides.js @@ -27,7 +27,7 @@ frappe.ui.Slide = class Slide {
- + Add More
@@ -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( $(``)); @@ -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() { diff --git a/frappe/public/js/frappe/utils/datetime.js b/frappe/public/js/frappe/utils/datetime.js index 8481fec5c5..12badc5353 100644 --- a/frappe/public/js/frappe/utils/datetime.js +++ b/frappe/public/js/frappe/utils/datetime.js @@ -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(); }, diff --git a/frappe/public/js/frappe/utils/number_format.js b/frappe/public/js/frappe/utils/number_format.js index b8a18610bd..57022b22c5 100644 --- a/frappe/public/js/frappe/utils/number_format.js +++ b/frappe/public/js/frappe/utils/number_format.js @@ -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) { diff --git a/frappe/public/js/frappe/views/calendar/calendar.js b/frappe/public/js/frappe/views/calendar/calendar.js index b84d37cff4..71ac175ba0 100644 --- a/frappe/public/js/frappe/views/calendar/calendar.js +++ b/frappe/public/js/frappe/views/calendar/calendar.js @@ -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 { diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index a7505ff8f3..0771306298 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -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) => { diff --git a/frappe/public/less/common.less b/frappe/public/less/common.less index 0f49c43de1..d6dc56cf6c 100644 --- a/frappe/public/less/common.less +++ b/frappe/public/less/common.less @@ -28,6 +28,10 @@ p { margin: 10px 0; } +details > summary { + cursor: pointer; +} + .text-color { color: @text-color !important; } diff --git a/frappe/public/less/desk.less b/frappe/public/less/desk.less index d8f5a141f4..0a8062efa2 100644 --- a/frappe/public/less/desk.less +++ b/frappe/public/less/desk.less @@ -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 { diff --git a/frappe/public/less/form.less b/frappe/public/less/form.less index 9c40131067..9a0eec46ea 100644 --- a/frappe/public/less/form.less +++ b/frappe/public/less/form.less @@ -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 { diff --git a/frappe/public/less/list.less b/frappe/public/less/list.less index bd6ea7e9d0..ea292daca2 100644 --- a/frappe/public/less/list.less +++ b/frappe/public/less/list.less @@ -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; -} \ No newline at end of file +} diff --git a/frappe/tests/test_global_search.py b/frappe/tests/test_global_search.py index 3fcf1fe9ed..01067c85dd 100644 --- a/frappe/tests/test_global_search.py +++ b/frappe/tests/test_global_search.py @@ -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() diff --git a/frappe/tests/test_listview.py b/frappe/tests/test_listview.py index 5c0657e9ee..3a73301608 100644 --- a/frappe/tests/test_listview.py +++ b/frappe/tests/test_listview.py @@ -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) \ No newline at end of file diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index 29cfcf5bac..83b5f5aa60 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -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() diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py index ff23a09c1a..7012b737c0 100644 --- a/frappe/utils/global_search.py +++ b/frappe/utils/global_search.py @@ -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): """ diff --git a/frappe/utils/oauth.py b/frappe/utils/oauth.py index 79bcee7a74..969623c369 100644 --- a/frappe/utils/oauth.py +++ b/frappe/utils/oauth.py @@ -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): diff --git a/frappe/utils/response.py b/frappe/utils/response.py index 8169986e44..1dfbbe5516 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -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' diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py index 7c0a642cb3..708bec887f 100755 --- a/frappe/utils/scheduler.py +++ b/frappe/utils/scheduler.py @@ -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