diff --git a/cypress/integration/api.js b/cypress/integration/api.js index 767cfbb55e..7a5b1611b0 100644 --- a/cypress/integration/api.js +++ b/cypress/integration/api.js @@ -2,7 +2,7 @@ context('API Resources', () => { before(() => { cy.visit('/login'); cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); it('Creates two Comments', () => { diff --git a/cypress/integration/awesome_bar.js b/cypress/integration/awesome_bar.js index 55c8015bae..3e12101532 100644 --- a/cypress/integration/awesome_bar.js +++ b/cypress/integration/awesome_bar.js @@ -2,7 +2,7 @@ context('Awesome Bar', () => { before(() => { cy.visit('/login'); cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); beforeEach(() => { diff --git a/cypress/integration/control_barcode.js b/cypress/integration/control_barcode.js index 674d825504..1df5e64f0e 100644 --- a/cypress/integration/control_barcode.js +++ b/cypress/integration/control_barcode.js @@ -1,7 +1,7 @@ context('Control Barcode', () => { beforeEach(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); function get_dialog_with_barcode() { diff --git a/cypress/integration/control_duration.js b/cypress/integration/control_duration.js index 4d974018d3..266d421e70 100644 --- a/cypress/integration/control_duration.js +++ b/cypress/integration/control_duration.js @@ -1,7 +1,7 @@ context('Control Duration', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); function get_dialog_with_duration(hide_days = 0, hide_seconds = 0) { diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js index 702c8430d6..918155b99b 100644 --- a/cypress/integration/control_link.js +++ b/cypress/integration/control_link.js @@ -1,11 +1,11 @@ context('Control Link', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); beforeEach(() => { - cy.visit('/app/space/Website'); + cy.visit('/app/website'); cy.create_records({ doctype: 'ToDo', description: 'this is a test todo for link' diff --git a/cypress/integration/control_rating.js b/cypress/integration/control_rating.js index 02d8e0639e..31c036d240 100644 --- a/cypress/integration/control_rating.js +++ b/cypress/integration/control_rating.js @@ -1,7 +1,7 @@ context('Control Rating', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); function get_dialog_with_rating() { diff --git a/cypress/integration/datetime.js b/cypress/integration/datetime.js index 74d823a81f..d66680b96b 100644 --- a/cypress/integration/datetime.js +++ b/cypress/integration/datetime.js @@ -4,7 +4,7 @@ const doctype_name = datetime_doctype.name; context('Control Date, Time and DateTime', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); return cy.insert_doc('DocType', datetime_doctype, true); }); diff --git a/cypress/integration/depends_on.js b/cypress/integration/depends_on.js index 7031634d98..1a6e1082aa 100644 --- a/cypress/integration/depends_on.js +++ b/cypress/integration/depends_on.js @@ -1,7 +1,7 @@ context('Depends On', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); return cy.window().its('frappe').then(frappe => { return frappe.call('frappe.tests.ui_test_helpers.create_doctype', { name: 'Test Depends On', diff --git a/cypress/integration/file_uploader.js b/cypress/integration/file_uploader.js index 5ef041b797..6bfe30ff55 100644 --- a/cypress/integration/file_uploader.js +++ b/cypress/integration/file_uploader.js @@ -1,7 +1,7 @@ context('FileUploader', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); function open_upload_dialog() { diff --git a/cypress/integration/form.js b/cypress/integration/form.js index f574770520..bea987b36d 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -1,7 +1,7 @@ context('Form', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); return cy.window().its('frappe').then(frappe => { return frappe.call("frappe.tests.ui_test_helpers.create_contact_records"); }); diff --git a/cypress/integration/grid_pagination.js b/cypress/integration/grid_pagination.js index 87c0fb0af4..8f6b79c1f4 100644 --- a/cypress/integration/grid_pagination.js +++ b/cypress/integration/grid_pagination.js @@ -1,11 +1,11 @@ context('Grid Pagination', () => { beforeEach(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); return cy.window().its('frappe').then(frappe => { return frappe.call("frappe.tests.ui_test_helpers.create_contact_phone_nos_records"); }); diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js index a1ba1cf25e..ac51f42ccb 100644 --- a/cypress/integration/list_view.js +++ b/cypress/integration/list_view.js @@ -1,7 +1,7 @@ context('List View', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); return cy.window().its('frappe').then(frappe => { return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow"); }); diff --git a/cypress/integration/list_view_settings.js b/cypress/integration/list_view_settings.js index cdb1c5d778..123d7e70f2 100644 --- a/cypress/integration/list_view_settings.js +++ b/cypress/integration/list_view_settings.js @@ -1,7 +1,7 @@ context('List View Settings', () => { beforeEach(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); it('Default settings', () => { cy.visit('/app/List/DocType/List'); diff --git a/cypress/integration/login.js b/cypress/integration/login.js index 4eb8933771..d1ee99b36d 100644 --- a/cypress/integration/login.js +++ b/cypress/integration/login.js @@ -35,7 +35,7 @@ context('Login', () => { cy.get('#login_password').type(Cypress.config('adminPassword')); cy.get('.btn-login:visible').click(); - cy.location('pathname').should('eq', '/app/space/Home'); + cy.location('pathname').should('eq', '/app/home'); cy.window().its('frappe.session.user').should('eq', 'Administrator'); }); diff --git a/cypress/integration/query_report.js b/cypress/integration/query_report.js index d0ca844362..e2a1c3fc79 100644 --- a/cypress/integration/query_report.js +++ b/cypress/integration/query_report.js @@ -1,7 +1,7 @@ context('Query Report', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); it('add custom column in report', () => { diff --git a/cypress/integration/recorder.js b/cypress/integration/recorder.js index 2c71dbe64d..4b86844d2a 100644 --- a/cypress/integration/recorder.js +++ b/cypress/integration/recorder.js @@ -4,7 +4,7 @@ context('Recorder', () => { }); it('Navigate to Recorder', () => { - cy.visit('/app/space/Website'); + cy.visit('/app/website'); cy.awesomebar('recorder'); cy.get('h1').should('contain', 'Recorder'); cy.location('pathname').should('eq', '#recorder'); diff --git a/cypress/integration/relative_time_filters.js b/cypress/integration/relative_time_filters.js index 33ea49f2d2..380d488547 100644 --- a/cypress/integration/relative_time_filters.js +++ b/cypress/integration/relative_time_filters.js @@ -4,7 +4,7 @@ context('Relative Timeframe', () => { }); before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); cy.window().its('frappe').then(frappe => { frappe.call("frappe.tests.ui_test_helpers.create_todo_records"); }); diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js index 00816a4fa6..d1a3e4dfd8 100644 --- a/cypress/integration/report_view.js +++ b/cypress/integration/report_view.js @@ -4,7 +4,7 @@ const doctype_name = custom_submittable_doctype.name; context('Report View', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); cy.insert_doc('DocType', custom_submittable_doctype, true); cy.clear_cache(); cy.insert_doc(doctype_name, { diff --git a/frappe/boot.py b/frappe/boot.py index 2f4569dee9..8cf75e02bb 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -109,7 +109,7 @@ def load_conf_settings(bootinfo): def load_desktop_data(bootinfo): from frappe.desk.desktop import get_desk_sidebar_items - bootinfo.allowed_workspaces = get_desk_sidebar_items(flatten=True, cache=False) + bootinfo.allowed_workspaces = get_desk_sidebar_items() bootinfo.module_page_map = get_controller("Workspace").get_module_page_map() bootinfo.dashboards = frappe.get_all("Dashboard") @@ -250,13 +250,12 @@ def add_home_page(bootinfo, docs): try: page = frappe.desk.desk_page.get(home_page) + docs.append(page) + bootinfo['home_page'] = page.name except (frappe.DoesNotExistError, frappe.PermissionError): if frappe.message_log: frappe.message_log.pop() - page = frappe.desk.desk_page.get('space') - - bootinfo['home_page'] = page.name - docs.append(page) + bootinfo['home_page'] = 'Workspaces' def add_timezone_info(bootinfo): system = bootinfo.sysdefaults.get("time_zone") diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index a59bef9fc7..09187b5467 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -26,6 +26,7 @@ from frappe.database.schema import validate_column_name, validate_column_length from frappe.model.docfield import supports_translation from frappe.modules.import_file import get_file_path from frappe.model.meta import Meta +from frappe.desk.utils import validate_route_conflict class InvalidFieldNameError(frappe.ValidationError): pass class UniqueFieldnameError(frappe.ValidationError): pass @@ -648,6 +649,8 @@ class DocType(Document): if not re.match("^(?![\W])[^\d_\s][\w ]+$", name, **flags): frappe.throw(_("DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores"), frappe.NameError) + validate_route_conflict(self.doctype, self.name) + def validate_links_table_fieldnames(meta): """Validate fieldnames in Links table""" if frappe.flags.in_patch: return diff --git a/frappe/core/doctype/doctype/patches/set_route.py b/frappe/core/doctype/doctype/patches/set_route.py index 655935f861..c052a51f38 100644 --- a/frappe/core/doctype/doctype/patches/set_route.py +++ b/frappe/core/doctype/doctype/patches/set_route.py @@ -1,7 +1,7 @@ import frappe -from frappe.desk.utils import get_doctype_route +from frappe.desk.utils import slug def execute(): for doctype in frappe.get_all('DocType', ['name', 'route'], dict(istable=0)): if not doctype.route: - frappe.db.set_value('DocType', doctype.name, 'route', get_doctype_route(doctype.name), update_modified = False) \ No newline at end of file + frappe.db.set_value('DocType', doctype.name, 'route', slug(doctype.name), update_modified = False) \ No newline at end of file diff --git a/frappe/core/doctype/page/page.py b/frappe/core/doctype/page/page.py index 2d616542f3..bdec350efd 100644 --- a/frappe/core/doctype/page/page.py +++ b/frappe/core/doctype/page/page.py @@ -9,6 +9,7 @@ from frappe.build import html_to_js_template from frappe.model.utils import render_include from frappe import conf, _, safe_decode from frappe.desk.form.meta import get_code_files_via_hooks, get_js +from frappe.desk.utils import validate_route_conflict from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles from six import text_type @@ -33,10 +34,7 @@ class Page(Document): self.name += '-' + str(cnt) def validate(self): - if frappe.db.get_value('DocType', self.name): - frappe.throw( - _("{} is the name of a DocType. DocType names cannot be the same as a Page name, please choose another name.").format(self.page_name) - ) + validate_route_conflict(self.doctype, self.name) if self.is_new() and not getattr(conf,'developer_mode', 0): frappe.throw(_("Not in Developer Mode")) diff --git a/frappe/core/doctype/page/patches/drop_unused_pages.py b/frappe/core/doctype/page/patches/drop_unused_pages.py new file mode 100644 index 0000000000..93b47cebcc --- /dev/null +++ b/frappe/core/doctype/page/patches/drop_unused_pages.py @@ -0,0 +1,5 @@ +import frappe + +def execute(): + for name in ('desktop', 'space'): + frappe.delete_doc('Page', name) \ No newline at end of file diff --git a/frappe/core/doctype/page/test_page.py b/frappe/core/doctype/page/test_page.py index 78659f1ffd..f7b3952a5b 100644 --- a/frappe/core/doctype/page/test_page.py +++ b/frappe/core/doctype/page/test_page.py @@ -8,4 +8,6 @@ import unittest test_records = frappe.get_test_records('Page') class TestPage(unittest.TestCase): - pass + def test_naming(self): + self.assertRaises(frappe.NameError, frappe.get_doc(dict(doctype='Page', page_name='DocType', module='Core')).insert) + self.assertRaises(frappe.NameError, frappe.get_doc(dict(doctype='Page', page_name='Settings', module='Core')).insert) diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 957cbbf72d..aac8b3deed 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -81,6 +81,7 @@ class TestServerScript(unittest.TestCase): def tearDownClass(cls): frappe.db.commit() frappe.db.sql('truncate `tabServer Script`') + frappe.cache().delete_value('server_script_map') def setUp(self): frappe.cache().delete_value('server_script_map') diff --git a/frappe/core/page/desktop/__init__.py b/frappe/core/page/desktop/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/core/page/desktop/desktop.js b/frappe/core/page/desktop/desktop.js deleted file mode 100644 index cc36a5a4e9..0000000000 --- a/frappe/core/page/desktop/desktop.js +++ /dev/null @@ -1,3 +0,0 @@ -frappe.pages['desktop'].on_page_load = function() { - frappe.utils.set_title(__("Home")); -}; \ No newline at end of file diff --git a/frappe/core/page/desktop/desktop.json b/frappe/core/page/desktop/desktop.json deleted file mode 100644 index 66bbfbfd40..0000000000 --- a/frappe/core/page/desktop/desktop.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "content": null, - "creation": "2019-01-29 13:11:48.872579", - "docstatus": 0, - "doctype": "Page", - "icon": "icon-th", - "idx": 0, - "modified": "2019-01-29 13:11:48.872579", - "modified_by": "Administrator", - "module": "Core", - "name": "desktop", - "owner": "Administrator", - "page_name": "desktop", - "roles": [ - { - "role": "All" - } - ], - "script": null, - "standard": "Yes", - "style": null, - "system_page": 0, - "title": "Desktop" -} \ No newline at end of file diff --git a/frappe/core/page/space/__init__.py b/frappe/core/page/space/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/core/page/space/space.js b/frappe/core/page/space/space.js deleted file mode 100644 index e781c56d8b..0000000000 --- a/frappe/core/page/space/space.js +++ /dev/null @@ -1,12 +0,0 @@ -frappe.pages['space'].on_page_load = function (wrapper) { - frappe.ui.make_app_page({ - parent: wrapper, - name: 'space', - title: __("Workspace"), - }); - - frappe.workspace = new frappe.views.Workspace(wrapper); - $(wrapper).bind('show', function () { - frappe.workspace.show(); - }); -} \ No newline at end of file diff --git a/frappe/core/page/space/space.json b/frappe/core/page/space/space.json deleted file mode 100644 index d5f0b4c9db..0000000000 --- a/frappe/core/page/space/space.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "content": null, - "creation": "2020-02-27 15:07:57.124916", - "docstatus": 0, - "doctype": "Page", - "icon": "icon-th", - "idx": 0, - "modified": "2020-12-16 14:22:05.591912", - "modified_by": "Administrator", - "module": "Core", - "name": "space", - "owner": "Administrator", - "page_name": "space", - "roles": [ - { - "role": "All" - } - ], - "script": null, - "standard": "Yes", - "style": null, - "system_page": 0 -} \ No newline at end of file diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.py b/frappe/custom/doctype/doctype_layout/doctype_layout.py index b580ac8f56..6a080b8fa2 100644 --- a/frappe/custom/doctype/doctype_layout/doctype_layout.py +++ b/frappe/custom/doctype/doctype_layout/doctype_layout.py @@ -7,9 +7,9 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document -from frappe.desk.utils import get_doctype_route +from frappe.desk.utils import slug class DocTypeLayout(Document): def validate(self): if not self.route: - self.route = get_doctype_route(self.name) + self.route = slug(self.name) diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index 1fa3f61752..f1bf7832ae 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -361,57 +361,39 @@ def get_desktop_page(page): } @frappe.whitelist() -def get_desk_sidebar_items(flatten=False, cache=True): - """Get list of sidebar items for desk - """ - pages = [] - _cache = frappe.cache() - if cache: - pages = _cache.get_value("desk_sidebar_items", user=frappe.session.user) - - if not pages or not cache: - # don't get domain restricted pages - blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules() - - filters = { - 'restrict_to_domain': ['in', frappe.get_active_domains()], - 'extends_another_page': 0, - 'for_user': '', - 'module': ['not in', blocked_modules] - } +def get_desk_sidebar_items(): + """Get list of sidebar items for desk""" - if not frappe.local.conf.developer_mode: - filters['developer_mode_only'] = '0' + # don't get domain restricted pages + blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules() - # pages sorted based on pinned to top and then by name - order_by = "pin_to_top desc, pin_to_bottom asc, name asc" - all_pages = frappe.get_all("Workspace", fields=["name", "category", "icon", "module"], - filters=filters, order_by=order_by, ignore_permissions=True) - pages = [] - - # Filter Page based on Permission - for page in all_pages: - try: - wspace = Workspace(page.get('name'), True) - if wspace.is_page_allowed(): - pages.append(page) - except frappe.PermissionError: - pass - - _cache.set_value("desk_sidebar_items", pages, frappe.session.user) + filters = { + 'restrict_to_domain': ['in', frappe.get_active_domains()], + 'extends_another_page': 0, + 'for_user': '', + 'module': ['not in', blocked_modules] + } - if flatten: - return pages + if not frappe.local.conf.developer_mode: + filters['developer_mode_only'] = '0' - from collections import defaultdict - sidebar_items = defaultdict(list) + # pages sorted based on pinned to top and then by name + order_by = "pin_to_top desc, pin_to_bottom asc, name asc" + all_pages = frappe.get_all("Workspace", fields=["name", "category", "icon", "module"], + filters=filters, order_by=order_by, ignore_permissions=True) + pages = [] - # The order will be maintained while categorizing - for page in pages: - # Translate label - page['label'] = _(page.get('name')) - sidebar_items[page["category"]].append(page) - return sidebar_items + # Filter Page based on Permission + for page in all_pages: + try: + wspace = Workspace(page.get('name'), True) + if wspace.is_page_allowed(): + pages.append(page) + page['label'] = _(page.get('name')) + except frappe.PermissionError: + pass + + return pages def get_table_with_counts(): counts = frappe.cache().get_value("information_schema:counts") diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 3c7ba10bcb..dcecfc4b5a 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -8,6 +8,7 @@ from frappe import _, _dict from frappe.utils.data import validate_json_string from frappe.modules.export_file import export_to_files from frappe.model.document import Document +from frappe.desk.utils import validate_route_conflict from json import loads, dumps @@ -15,6 +16,7 @@ class Workspace(Document): def validate(self): if (self.is_standard and not frappe.conf.developer_mode and not disable_saving_as_standard()): frappe.throw(_("You need to be in developer mode to edit this document")) + validate_route_conflict(self.doctype, self.name) def on_update(self): if disable_saving_as_standard(): diff --git a/frappe/desk/utils.py b/frappe/desk/utils.py index a3de00cd54..7281f456b0 100644 --- a/frappe/desk/utils.py +++ b/frappe/desk/utils.py @@ -3,5 +3,17 @@ import frappe -def get_doctype_route(name): +def validate_route_conflict(doctype, name): + ''' + Raises exception if name clashes with routes from other documents for /app routing + ''' + all_names = [] + for _doctype in ['Page', 'Workspace', 'DocType']: + all_names.extend([slug(d) for d in frappe.get_all(_doctype, pluck='name') if (doctype != _doctype and d != name)]) + + if slug(name) in all_names: + frappe.msgprint(frappe._('Name already taken, please set a new name')) + raise frappe.NameError + +def slug(name): return name.lower().replace(' ', '-') \ No newline at end of file diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py index ee7f123b7e..d6eab6e2c3 100644 --- a/frappe/email/doctype/newsletter/test_newsletter.py +++ b/frappe/email/doctype/newsletter/test_newsletter.py @@ -15,21 +15,7 @@ emails = ["test_subscriber1@example.com", "test_subscriber2@example.com", class TestNewsletter(unittest.TestCase): def setUp(self): - frappe.set_user("Administrator") - frappe.db.sql('delete from `tabEmail Group Member`') - - group_exist=frappe.db.exists("Email Group", "_Test Email Group") - if len(group_exist) == 0: - frappe.get_doc({ - "doctype": "Email Group", - "title": "_Test Email Group" - }).insert() - for email in emails: - frappe.get_doc({ - "doctype": "Email Group Member", - "email": email, - "email_group": "_Test Email Group" - }).insert() + self.make_email_group() def test_send(self): name = self.send_newsletter() @@ -46,8 +32,9 @@ class TestNewsletter(unittest.TestCase): from frappe.email.queue import flush flush(from_test=True) to_unsubscribe = unquote(frappe.local.flags.signed_query_string.split("email=")[1].split("&")[0]) - group = frappe.get_all("Newsletter Email Group", filters={"parent" : name}, fields=["email_group"]) - confirmed_unsubscribe(to_unsubscribe, group[0].email_group) + + email_group = frappe.db.get_value('Newsletter Email Group', dict(parent=name), 'email_group') + confirmed_unsubscribe(to_unsubscribe, email_group) name = self.send_newsletter() @@ -58,6 +45,36 @@ class TestNewsletter(unittest.TestCase): if email != to_unsubscribe: self.assertTrue(email in recipients) + frappe.db.set_value('Email Group Member', dict(email=to_unsubscribe), 'unsubscribed', 0) + + def test_portal(self): + self.send_newsletter(1) + frappe.set_user("test1@example.com") + from frappe.email.doctype.newsletter.newsletter import get_newsletter_list + newsletters = get_newsletter_list("Newsletter", None, None, 0) + self.assertEqual(len(newsletters), 1) + frappe.set_user("Administrator") + + + def test_newsletter_context(self): + context = frappe._dict() + newsletter_name = self.send_newsletter(1) + frappe.set_user("test2@example.com") + doc = frappe.get_doc("Newsletter", newsletter_name) + doc.get_context(context) + self.assertEqual(context.no_cache, 1) + self.assertTrue("attachments" not in list(context)) + frappe.set_user("Administrator") + + def test_schedule_send(self): + self.send_newsletter(schedule_send=add_days(getdate(), -1)) + + email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")] + self.assertEqual(len(email_queue_list), 4) + recipients = [e.recipients[0].recipient for e in email_queue_list] + for email in emails: + self.assertTrue(email in recipients) + @staticmethod def send_newsletter(published=0, schedule_send=None): frappe.db.sql("delete from `tabEmail Queue`") @@ -83,27 +100,15 @@ class TestNewsletter(unittest.TestCase): newsletter.send_emails() return newsletter.name - def test_portal(self): - self.send_newsletter(1) - frappe.set_user("test1@example.com") - from frappe.email.doctype.newsletter.newsletter import get_newsletter_list - newsletters = get_newsletter_list("Newsletter", None, None, 0) - self.assertEqual(len(newsletters), 1) - - def test_newsletter_context(self): - context = frappe._dict() - newsletter_name = self.send_newsletter(1) - frappe.set_user("test2@example.com") - doc = frappe.get_doc("Newsletter", newsletter_name) - doc.get_context(context) - self.assertEqual(context.no_cache, 1) - self.assertTrue("attachments" not in list(context)) - - def test_schedule_send(self): - self.send_newsletter(schedule_send=add_days(getdate(), -1)) - - email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")] - self.assertEqual(len(email_queue_list), 4) - recipients = [e.recipients[0].recipient for e in email_queue_list] + def make_email_group(self): + email_group = "_Test Email Group" + if not frappe.db.exists("Email Group", email_group): + frappe.get_doc("Email Group", email_group).insert() + for email in emails: - self.assertTrue(email in recipients) \ No newline at end of file + if not frappe.db.exists('Email Group Member', dict(email=email, email_group = email_group)): + frappe.get_doc({ + "doctype": "Email Group Member", + "email": email, + "email_group": email_group + }).insert() diff --git a/frappe/patches.txt b/frappe/patches.txt index 08b30be90a..8617eac40d 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -323,3 +323,4 @@ frappe.patches.v13_0.update_icons_in_customized_desk_pages execute:frappe.db.set_default('desktop:home_page', 'space') execute:frappe.delete_doc_if_exists('Page', 'workspace') execute:frappe.delete_doc_if_exists('Page', 'dashboard', force=1) +frappe.core.doctype.page.patches.drop_unused_pages diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 16f54c1b13..85b29a5652 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -253,10 +253,7 @@ frappe.Application = Class.extend({ }, load_bootinfo: function() { if(frappe.boot) { - frappe.modules = {}; - (frappe.boot.allowed_workspaces || []).forEach(function(m) { - frappe.modules[m.module]=m; - }); + this.setup_workspaces(); frappe.model.sync(frappe.boot.docs); $.extend(frappe._messages, frappe.boot.__messages); this.check_metadata_cache_status(); @@ -278,6 +275,19 @@ frappe.Application = Class.extend({ } }, + setup_workspaces() { + frappe.modules = {}; + frappe.workspaces = {}; + for (let page of frappe.boot.allowed_workspaces || []) { + frappe.modules[page.module]=page; + frappe.workspaces[frappe.router.slug(page.name)] = page; + } + if (!frappe.workspaces['home']) { + // default workspace is settings for Frappe + frappe.workspaces['home'] = frappe.workspaces['settings']; + } + }, + load_user_permissions: function() { frappe.defaults.update_user_permissions(); diff --git a/frappe/public/js/frappe/list/views.js b/frappe/public/js/frappe/list/views.js index 7eada35d42..060f43488a 100644 --- a/frappe/public/js/frappe/list/views.js +++ b/frappe/public/js/frappe/list/views.js @@ -35,7 +35,7 @@ frappe.views.Views = class Views { } set_route(view, calendar_name) { - const route = [this.get_doctype_route(), 'view', view]; + const route = [this.slug(), 'view', view]; if (calendar_name) route.push(calendar_name); frappe.set_route(route); } @@ -233,11 +233,11 @@ frappe.views.Views = class Views { // has standard calendar view calendars.push({ name: 'Default', - route: `/app/${this.get_doctype_route()}/view/calendar/default` + route: `/app/${this.slug()}/view/calendar/default` }); } result.map(calendar => { - calendars.push({name: calendar.name, route: `/app/${this.get_doctype_route()}/view/calendar/${calendar.name}`}); + calendars.push({name: calendar.name, route: `/app/${this.slug()}/view/calendar/${calendar.name}`}); }); return calendars; @@ -263,7 +263,7 @@ frappe.views.Views = class Views { return accounts_to_add; } - get_doctype_route() { + slug() { return frappe.router.slug(frappe.router.doctype_layout || this.doctype); } } \ No newline at end of file diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index 4ececbb7ca..8c19c57876 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -117,40 +117,50 @@ frappe.router = { }, convert_to_standard_route(route) { + // /app/settings = ["Workspaces", "Settings"] // /app/user = ["List", "User"] // /app/user/view/report = ["List", "User", "Report"] // /app/user/view/tree = ["Tree", "User"] // /app/user/user-001 = ["Form", "User", "user-001"] // /app/user/user-001 = ["Form", "User", "user-001"] // /app/event/view/calendar/default = ["List", "Event", "Calendar", "Default"] - let standard_route = route; - let doctype_route = this.routes[route[0]]; - if (doctype_route) { - // doctype route - if (route[1]) { - if (route[2] && route[1]==='view') { - standard_route = this.get_standard_route_for_list(route, doctype_route); - } else { - let docname = route[1]; - if (route.length > 2) { - docname = route.slice(1).join('/'); - } - standard_route = ['Form', doctype_route.doctype, docname]; - } - } else if (frappe.model.is_single(doctype_route.doctype)) { - standard_route = ['Form', doctype_route.doctype, doctype_route.doctype]; + if (frappe.workspaces[route[0]]) { + // workspace + route = ['Workspaces', frappe.workspaces[route[0]].name]; + } else if (this.routes[route[0]]) { + // route + route = this.set_doctype_route(route); + } + + return route; + }, + + set_doctype_route(route) { + let doctype_route = this.routes[route[0]]; + // doctype route + if (route[1]) { + if (route[2] && route[1]==='view') { + route = this.get_standard_route_for_list(route, doctype_route); } else { - standard_route = ['List', doctype_route.doctype, 'List']; + let docname = route[1]; + if (route.length > 2) { + docname = route.slice(1).join('/'); + } + route = ['Form', doctype_route.doctype, docname]; } + } else if (frappe.model.is_single(doctype_route.doctype)) { + route = ['Form', doctype_route.doctype, doctype_route.doctype]; + } else { + route = ['List', doctype_route.doctype, 'List']; + } - if (doctype_route.doctype_layout) { - // set the layout - this.doctype_layout = doctype_route.doctype_layout; - } + if (doctype_route.doctype_layout) { + // set the layout + this.doctype_layout = doctype_route.doctype_layout; } - return standard_route; + return route; }, get_standard_route_for_list(route, doctype_route) { @@ -186,14 +196,8 @@ frappe.router = { // create the page generator (factory) object and call `show` // if there is no generator, render the `Page` object - // first the router needs to know if its a "page", "doctype", "space" - const route = this.current_route; const factory = frappe.utils.to_title_case(route[0]); - if (factory === 'Workspace') { - frappe.views.pageview.show(''); - return; - } if (route[1] && frappe.views[factory + "Factory"]) { route[0] = factory; @@ -329,7 +333,7 @@ frappe.router = { }, make_url(params) { - return '/app/' + $.map(params, function(a) { + let path_string = $.map(params, function(a) { if ($.isPlainObject(a)) { frappe.route_options = a; return null; @@ -342,6 +346,8 @@ frappe.router = { return a; } }).join('/'); + + return '/app/' + (path_string || 'home'); }, push_state(url) { diff --git a/frappe/public/js/frappe/views/breadcrumbs.js b/frappe/public/js/frappe/views/breadcrumbs.js index b845fb3e8b..4f2a9d2163 100644 --- a/frappe/public/js/frappe/views/breadcrumbs.js +++ b/frappe/public/js/frappe/views/breadcrumbs.js @@ -88,8 +88,7 @@ frappe.breadcrumbs = { if (breadcrumbs.workspace) { if(!breadcrumbs.module_info.blocked && frappe.visible_modules.includes(breadcrumbs.module_info.module)) { - $(repl('
  • %(label)s
  • ', - { module: breadcrumbs.workspace, label: __(breadcrumbs.workspace) })) + $(`
  • ${__(breadcrumbs.workspace)}
  • `) .appendTo(this.$breadcrumbs); } } diff --git a/frappe/public/js/frappe/views/pageview.js b/frappe/public/js/frappe/views/pageview.js index 48a2780fac..6fd91d73c4 100644 --- a/frappe/public/js/frappe/views/pageview.js +++ b/frappe/public/js/frappe/views/pageview.js @@ -6,18 +6,18 @@ frappe.provide("frappe.standard_pages"); frappe.views.pageview = { with_page: function(name, callback) { - if(in_list(Object.keys(frappe.standard_pages), name)) { - if(!frappe.pages[name]) { + if (frappe.standard_pages[name]) { + if (!frappe.pages[name]) { frappe.standard_pages[name](); } callback(); return; } - if((locals.Page && locals.Page[name] && locals.Page[name].script) || name==window.page_name) { + if ((locals.Page && locals.Page[name] && locals.Page[name].script) || name==window.page_name) { // already loaded callback(); - } else if(localStorage["_page:" + name] && frappe.boot.developer_mode!=1) { + } else if (localStorage["_page:" + name] && frappe.boot.developer_mode!=1) { // cached in local storage frappe.model.sync(JSON.parse(localStorage["_page:" + name])); callback(); @@ -27,7 +27,7 @@ frappe.views.pageview = { method: 'frappe.desk.desk_page.getpage', args: {'name':name }, callback: function(r) { - if(!r.docs._dynamic_page) { + if (!r.docs._dynamic_page) { localStorage["_page:" + name] = JSON.stringify(r.docs); } callback(); @@ -61,14 +61,14 @@ frappe.views.Page = class Page { var me = this; // web home page - if(name==window.page_name) { + if (name==window.page_name) { this.wrapper = document.getElementById('page-' + name); this.wrapper.label = document.title || window.page_name; this.wrapper.page_name = window.page_name; frappe.pages[window.page_name] = this.wrapper; } else { this.pagedoc = locals.Page[this.name]; - if(!this.pagedoc) { + if (!this.pagedoc) { frappe.show_not_found(name); return; } @@ -77,7 +77,7 @@ frappe.views.Page = class Page { this.wrapper.page_name = this.pagedoc.name; // set content, script and style - if(this.pagedoc.content) + if (this.pagedoc.content) this.wrapper.innerHTML = this.pagedoc.content; frappe.dom.eval(this.pagedoc.__script || this.pagedoc.script || ''); frappe.dom.set_style(this.pagedoc.style || ''); @@ -98,7 +98,7 @@ frappe.views.Page = class Page { trigger_page_event(eventname) { var me = this; - if(me.wrapper[eventname]) { + if (me.wrapper[eventname]) { me.wrapper[eventname](me.wrapper); } } @@ -123,11 +123,11 @@ frappe.show_not_permitted = function(page_name) { frappe.show_message_page = function(opts) { // opts can include `page_name`, `message`, `icon` or `img` - if(!opts.page_name) { + if (!opts.page_name) { opts.page_name = frappe.get_route_str(); } - if(opts.icon) { + if (opts.icon) { opts.img = repl(' ', opts); } else if (opts.img) { opts.img = repl('', opts); diff --git a/frappe/public/js/frappe/views/workspace/workspace.js b/frappe/public/js/frappe/views/workspace/workspace.js index ce6a0a73fc..2a359fece1 100644 --- a/frappe/public/js/frappe/views/workspace/workspace.js +++ b/frappe/public/js/frappe/views/workspace/workspace.js @@ -1,3 +1,18 @@ +frappe.standard_pages['Workspaces'] = function() { + var wrapper = frappe.container.add_page('Workspaces'); + + frappe.ui.make_app_page({ + parent: wrapper, + name: 'Workspaces', + title: __("Workspace"), + }); + + frappe.workspace = new frappe.views.Workspace(wrapper); + $(wrapper).bind('show', function () { + frappe.workspace.show(); + }); +}; + frappe.views.Workspace = class Workspace { constructor(wrapper) { this.wrapper = $(wrapper); @@ -14,15 +29,24 @@ frappe.views.Workspace = class Workspace { "Administration" ]; - this.fetch_desktop_settings().then(() => { - this.make_sidebar(); - }) + this.setup_workspaces(); + this.make_sidebar(); + } + + setup_workspaces() { + // workspaces grouped by categories + this.workspaces = {}; + for (let page of frappe.boot.allowed_workspaces) { + if (!this.workspaces[page.category]) { + this.workspaces[page.category] = []; + } + this.workspaces[page.category].push(page); + } } show() { let page = this.get_page_to_show(); this.page.set_title(`${__(page)}`); - frappe.set_route('space', page); this.show_page(page); } @@ -40,44 +64,22 @@ frappe.views.Workspace = class Workspace { if (localStorage.current_workspace) { default_page = localStorage.current_workspace; - } else if (this.desktop_settings) { - default_page = this.desktop_settings["Modules"][0].name; + } else if (this.workspaces) { + default_page = this.workspaces["Modules"][0].name; } else if (frappe.boot.allowed_workspaces) { default_page = frappe.boot.allowed_workspaces[0].name; } else { - default_page = "Website"; + default_page = "Settings"; } let page = frappe.get_route()[1] || default_page; - return page; } - fetch_desktop_settings() { - return frappe - .call("frappe.desk.desktop.get_desk_sidebar_items") - .then(response => { - if (response.message) { - this.desktop_settings = response.message; - } else { - frappe.throw({ - title: __("Couldn't Load Desk"), - message: - __("Something went wrong while loading Desk. Please relaod the page. If the problem persists, contact the Administrator"), - indicator: "red", - primary_action: { - label: __("Reload"), - action: () => location.reload() - } - }); - } - }); - } - make_sidebar() { this.sidebar_categories.forEach(category => { - if (this.desktop_settings[category]) { - this.build_sidebar_section(category, this.desktop_settings[category]) + if (this.workspaces[category]) { + this.build_sidebar_section(category, this.workspaces[category]); } }); } @@ -94,7 +96,7 @@ frappe.views.Workspace = class Workspace { const get_sidebar_item = function (item) { return $(` ${frappe.utils.icon(item.icon || "folder-normal", "md")} diff --git a/frappe/search/website_search.py b/frappe/search/website_search.py index de93fea3f5..87ef4b08ad 100644 --- a/frappe/search/website_search.py +++ b/frappe/search/website_search.py @@ -31,7 +31,7 @@ class WebsiteSearch(FullTextSearch): self (object): FullTextSearch Instance """ routes = get_static_pages_from_all_apps() - routes += get_doctype_routes_with_web_view() + routes += slugs_with_web_view() documents = [self.get_document_to_index(route) for route in routes] return documents @@ -74,7 +74,7 @@ class WebsiteSearch(FullTextSearch): ) -def get_doctype_routes_with_web_view(): +def slugs_with_web_view(): all_routes = [] filters = { "has_web_view": 1, "allow_guest_to_view": 1, "index_web_pages_for_search": 1} fields = ["name", "is_published_field"] diff --git a/frappe/tests/test_search.py b/frappe/tests/test_search.py index 5c17ce66c0..e39d6c4691 100644 --- a/frappe/tests/test_search.py +++ b/frappe/tests/test_search.py @@ -41,15 +41,15 @@ class TestSearch(unittest.TestCase): #Search for the word "pay", part of the word "pays" (country) in french. def test_link_search_in_foreign_language(self): - frappe.local.lang = 'fr' - search_widget(doctype="DocType", txt="pay", page_length=20) - output = frappe.response["values"] - - result = [['found' for x in y if x=="Country"] for y in output] - self.assertTrue(['found'] in result) - - def tearDown(self): - frappe.local.lang = 'en' + try: + frappe.local.lang = 'fr' + search_widget(doctype="DocType", txt="pay", page_length=20) + output = frappe.response["values"] + + result = [['found' for x in y if x=="Country"] for y in output] + self.assertTrue(['found'] in result) + finally: + frappe.local.lang = 'en' def test_validate_and_sanitize_search_inputs(self): diff --git a/frappe/tests/test_translate.py b/frappe/tests/test_translate.py index 4dcaf3e979..c29390c429 100644 --- a/frappe/tests/test_translate.py +++ b/frappe/tests/test_translate.py @@ -15,9 +15,12 @@ class TestTranslate(unittest.TestCase): self.assertListEqual(data, expected_output) def test_translation_with_context(self): - frappe.local.lang = 'fr' - self.assertEqual(_('Change'), 'Changement') - self.assertEqual(_('Change', context='Coins'), 'la monnaie') + try: + frappe.local.lang = 'fr' + self.assertEqual(_('Change'), 'Changement') + self.assertEqual(_('Change', context='Coins'), 'la monnaie') + finally: + frappe.local.lang = 'en' expected_output = [ ('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', 'This is some context', 2), diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 678fa956c2..9d2d98674d 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -17,7 +17,7 @@ from six.moves.urllib.parse import quote, urljoin from html2text import html2text from markdown2 import markdown, MarkdownError from six import iteritems, text_type, string_types, integer_types -from frappe.desk.utils import get_doctype_route +from frappe.desk.utils import slug DATE_FORMAT = "%Y-%m-%d" TIME_FORMAT = "%H:%M:%S.%f" @@ -1059,17 +1059,17 @@ def get_link_to_report(name, label=None, report_type=None, doctype=None, filters return """{1}""".format(get_url_to_report(name, report_type, doctype), label) def get_absolute_url(doctype, name): - return "/app/{0}/{1}".format(quoted(get_doctype_route(doctype)), quoted(name)) + return "/app/{0}/{1}".format(quoted(slug(doctype)), quoted(name)) def get_url_to_form(doctype, name): - return get_url(uri = "/app/{0}/{1}".format(quoted(get_doctype_route(doctype)), quoted(name))) + return get_url(uri = "/app/{0}/{1}".format(quoted(slug(doctype)), quoted(name))) def get_url_to_list(doctype): - return get_url(uri = "/app/{0}".format(quoted(get_doctype_route(doctype)))) + return get_url(uri = "/app/{0}".format(quoted(slug(doctype)))) def get_url_to_report(name, report_type = None, doctype = None): if report_type == "Report Builder": - return get_url(uri = "/app/{0}/view/report/{1}".format(quoted(get_doctype_route(doctype)), quoted(name))) + return get_url(uri = "/app/{0}/view/report/{1}".format(quoted(slug(doctype)), quoted(name))) else: return get_url(uri = "/app/query-report/{0}".format(quoted(name))) diff --git a/frappe/website/router.py b/frappe/website/router.py index 5244c57ba8..946c83811a 100644 --- a/frappe/website/router.py +++ b/frappe/website/router.py @@ -441,4 +441,4 @@ def get_doctypes_with_web_view(): return frappe.cache().get_value('doctypes_with_web_view', _get) def get_start_folders(): - return frappe.local.flags.web_pages_folders or ('www', 'templates/pages') + return frappe.local.flags.web_pages_folders or ('www', 'templates/pages') \ No newline at end of file