diff --git a/.github/workflows/release_notes.yml b/.github/workflows/release_notes.yml new file mode 100644 index 0000000000..bf761f23ba --- /dev/null +++ b/.github/workflows/release_notes.yml @@ -0,0 +1,38 @@ +# This action: +# +# 1. Generates release notes using github API. +# 2. Strips unnecessary info like chore/style etc from notes. +# 3. Updates release info. + +# This action needs to be maintained on all branches that do releases. + +name: 'Release Notes' + +on: + workflow_dispatch: + inputs: + tag_name: + description: 'Tag of release like v13.0.0' + required: true + type: string + release: + types: [released] + +permissions: + contents: read + +jobs: + regen-notes: + name: 'Regenerate release notes' + runs-on: ubuntu-latest + + steps: + - name: Update notes + run: | + NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/frappe/releases/generate-notes -f tag_name=$RELEASE_TAG | jq -r '.body' | sed -E '/^\* (chore|ci|test|docs|style)/d' ) + RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/frappe/releases/tags/$RELEASE_TAG | jq -r '.id') + gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/frappe/releases/$RELEASE_ID -f body=$NEW_NOTES + + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + RELEASE_TAG: ${{ github.event.inputs.tag_name || github.event.release.tag_name }} diff --git a/.releaserc b/.releaserc index c9ca71bbf5..86f4f3cda0 100644 --- a/.releaserc +++ b/.releaserc @@ -13,9 +13,9 @@ [ "@semantic-release/git", { "assets": ["frappe/__init__.py"], - "message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}" + "message": "chore(release): Bumped to Version ${nextRelease.version}" } ], "@semantic-release/github" ] -} \ No newline at end of file +} diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index e5c6c406eb..bb943c7223 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -532,7 +532,7 @@ def _mariadb(): command = [ mysql, "--port", - frappe.conf.db_port or MariaDBDatabase.default_port, + str(frappe.conf.db_port or MariaDBDatabase.default_port), "-u", frappe.conf.db_name, f"-p{frappe.conf.db_password}", @@ -548,7 +548,13 @@ def _mariadb(): def _psql(): psql = which("psql") - subprocess.run([psql, "-d", frappe.conf.db_name]) + + host = frappe.conf.db_host or "127.0.0.1" + port = frappe.conf.db_port or "5432" + env = os.environ.copy() + env["PGPASSWORD"] = frappe.conf.db_password + conn_string = f"postgresql://{frappe.conf.db_name}@{host}:{port}/{frappe.conf.db_name}" + subprocess.run([psql, conn_string], check=True, env=env) @click.command("jupyter") diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 36e7629173..e7d250148b 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -133,7 +133,7 @@ class Contact(Document): def get_default_contact(doctype, name): """Returns default contact for the given doctype, name""" out = frappe.db.sql( - '''select parent, + """select parent, IFNULL((select is_primary_contact from tabContact c where c.name = dl.parent), 0) as is_primary_contact from @@ -141,7 +141,7 @@ def get_default_contact(doctype, name): where dl.link_doctype=%s and dl.link_name=%s and - dl.parenttype = 'Contact' ''', + dl.parenttype = 'Contact' """, (doctype, name), as_dict=True, ) diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index 85de33841f..7b6427d1c2 100644 --- a/frappe/core/doctype/communication/mixins.py +++ b/frappe/core/doctype/communication/mixins.py @@ -59,10 +59,7 @@ class CommunicationEmailMixin: * if email copy is requested by sender, then add sender to CC. * If this doc is created through inbound mail, then add doc owner to cc list * remove all the thread_notify disabled users. - * Make sure that all users enabled in the system - * Remove admin from email list - - * FixMe: Removed adding TODO owners to cc list. Check if that is needed. + * Remove standard users from email list """ if hasattr(self, "_final_cc"): return self._final_cc @@ -80,13 +77,12 @@ class CommunicationEmailMixin: cc = set(cc) - set(self.filter_thread_notification_disbled_users(cc)) cc = cc - set(self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)) - cc = cc - set(self.filter_disabled_users(cc)) # # Incase of inbound mail, to and cc already received the mail, no need to send again. if is_inbound_mail_communcation: cc = cc - set(self.cc_list() + self.to_list()) - self._final_cc = list(filter(lambda id: id != "Administrator", cc)) + self._final_cc = [m for m in cc if m not in frappe.STANDARD_USERS] return self._final_cc def get_mail_cc_with_displayname(self, is_inbound_mail_communcation=False, include_sender=False): @@ -99,8 +95,7 @@ class CommunicationEmailMixin: """ * Thread_notify check * Email unsubscribe list - * User must be enabled in the system - * remove_administrator_from_email_list + * remove standard users. """ if hasattr(self, "_final_bcc"): return self._final_bcc @@ -110,13 +105,12 @@ class CommunicationEmailMixin: bcc = bcc - {self.sender_mailid} bcc = bcc - set(self.filter_thread_notification_disbled_users(bcc)) bcc = bcc - set(self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)) - bcc = bcc - set(self.filter_disabled_users(bcc)) # Incase of inbound mail, to and cc & bcc already received the mail, no need to send again. if is_inbound_mail_communcation: bcc = bcc - set(self.bcc_list() + self.to_list()) - self._final_bcc = list(filter(lambda id: id != "Administrator", bcc)) + self._final_bcc = [m for m in bcc if m not in frappe.STANDARD_USERS] return self._final_bcc def get_mail_bcc_with_displayname(self, is_inbound_mail_communcation=False): diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py index 7c032a926d..5b208eaeb7 100644 --- a/frappe/core/doctype/communication/test_communication.py +++ b/frappe/core/doctype/communication/test_communication.py @@ -343,7 +343,7 @@ class TestCommunicationEmailMixin(FrappeTestCase): user = self.new_user(email="bcc+2@test.com", enabled=0) comm = self.new_communication(bcc=bcc_list) res = comm.get_mail_bcc_with_displayname() - self.assertCountEqual(res, ["bcc+1@test.com"]) + self.assertCountEqual(res, bcc_list) user.delete() comm.delete() diff --git a/frappe/core/doctype/error_snapshot/error_snapshot.py b/frappe/core/doctype/error_snapshot/error_snapshot.py index 6966cf0aca..acc49c78cd 100644 --- a/frappe/core/doctype/error_snapshot/error_snapshot.py +++ b/frappe/core/doctype/error_snapshot/error_snapshot.py @@ -12,10 +12,10 @@ class ErrorSnapshot(Document): def onload(self): if not self.parent_error_snapshot: - self.db_set("seen", True, update_modified=False) + self.db_set("seen", 1, update_modified=False) for relapsed in frappe.get_all("Error Snapshot", filters={"parent_error_snapshot": self.name}): - frappe.db.set_value("Error Snapshot", relapsed.name, "seen", True, update_modified=False) + frappe.db.set_value("Error Snapshot", relapsed.name, "seen", 1, update_modified=False) frappe.local.flags.commit = True @@ -32,7 +32,7 @@ class ErrorSnapshot(Document): self.update({"parent_error_snapshot": parent["name"]}) frappe.db.set_value("Error Snapshot", parent["name"], "relapses", parent["relapses"] + 1) if parent["seen"]: - frappe.db.set_value("Error Snapshot", parent["name"], "seen", False) + frappe.db.set_value("Error Snapshot", parent["name"], "seen", 0) @staticmethod def clear_old_logs(days=30): diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index 9f3f53cad6..37cb977fdc 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -2,24 +2,6 @@ frappe.ui.form.Control = class BaseControl { constructor(opts) { $.extend(this, opts); this.make(); - - // if developer_mode=1, show fieldname as tooltip - if (frappe.boot.user && frappe.boot.developer_mode === 1 && this.$wrapper) { - this.$wrapper.attr("title", __(this.df.fieldname)); - } else if (this.$wrapper) { - // show fieldname as tooltip when cmd/ctrl key is pressed - $(document).on("keydown", (e) => { - if (e.metaKey) { - this.$wrapper.attr("title", __(this.df.fieldname)); - } - }); - $(document).on("keyup", (e) => { - if (!e.metaKey) { - this.$wrapper.removeAttr("title"); - } - }); - } - if (this.render_input) { this.refresh(); } @@ -31,6 +13,7 @@ frappe.ui.form.Control = class BaseControl { .attr("data-fieldname", this.df.fieldname); this.wrapper = this.$wrapper.get(0); this.wrapper.fieldobj = this; // reference for event handlers + this.$wrapper.append(`${__(this.df.fieldname)}`); } make_wrapper() { diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index e070e1db0b..10c9abb431 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -35,6 +35,7 @@ frappe.ui.form.Layout = class Layout { } this.setup_tab_events(); + this.setup_tooltip_events(); this.render(); } @@ -529,6 +530,19 @@ frappe.ui.form.Layout = class Layout { }); } + setup_tooltip_events() { + $(document).on("keydown", (e) => { + if (e.metaKey) { + this.wrapper.addClass("show-tooltip"); + } + }); + $(document).on("keyup", (e) => { + if (!e.metaKey) { + this.wrapper.removeClass("show-tooltip"); + } + }); + } + handle_tab(doctype, fieldname, shift) { let grid_row = null, prev = null, diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 272be5ea0f..76c99652c6 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -1256,8 +1256,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { // shift select checkboxes if (e.shiftKey && this.$checkbox_cursor && !$target.is(this.$checkbox_cursor)) { - const name_1 = this.$checkbox_cursor.data().name; - const name_2 = $target.data().name; + const name_1 = decodeURIComponent(this.$checkbox_cursor.data().name); + const name_2 = decodeURIComponent($target.data().name); const index_1 = this.data.findIndex((d) => d.name === name_1); const index_2 = this.data.findIndex((d) => d.name === name_2); let [min_index, max_index] = [index_1, index_2]; @@ -1268,7 +1268,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { let docnames = this.data.slice(min_index + 1, max_index).map((d) => d.name); const selector = docnames - .map((name) => `.list-row-checkbox[data-name="${name}"]`) + .map((name) => `.list-row-checkbox[data-name="${encodeURIComponent(name)}"]`) .join(","); this.$result.find(selector).prop("checked", true); } diff --git a/frappe/public/scss/desk/form.scss b/frappe/public/scss/desk/form.scss index 183c4b9033..b2cb19baf7 100644 --- a/frappe/public/scss/desk/form.scss +++ b/frappe/public/scss/desk/form.scss @@ -1,6 +1,29 @@ @import "../common/form.scss"; @import '~cropperjs/dist/cropper.min'; +.tooltip-content { + position: absolute; + bottom: 104%; + left: 0; + z-index: 9999; + padding: 2px 6px; + border-radius: var(--border-radius-sm); + background: var(--gray-dark); + color: var(--text-dark); + font-size: var(--text-xs); + opacity: 0; + cursor: default; + transition: opacity 0.3s, transform 3s; + pointer-events: none; +} + +.show-tooltip .frappe-control:hover .tooltip-content { + opacity: 1; + transform: translate3d(0,0,0); + pointer-events: auto; +} + + .std-form-layout > .form-layout > .form-page { border-radius: var(--border-radius-md); box-shadow: var(--card-shadow); diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py index 6582b29b7e..7af66f6c62 100644 --- a/frappe/tests/test_commands.py +++ b/frappe/tests/test_commands.py @@ -29,7 +29,7 @@ import frappe.recorder from frappe.installer import add_to_installed_apps, remove_app from frappe.query_builder.utils import db_type_is from frappe.tests.test_query_builder import run_only_if -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, timeout from frappe.utils import add_to_date, get_bench_path, get_bench_relative_path, now from frappe.utils.backups import BackupGenerator, fetch_latest_backups from frappe.utils.jinja_globals import bundled_asset @@ -763,3 +763,10 @@ class TestCommandUtils(FrappeTestCase): app_groups = get_app_groups() self.assertIn("frappe", app_groups) self.assertIsInstance(app_groups["frappe"], click.Group) + + +class TestDBCli(BaseTestCommands): + @timeout(10) + def test_db_cli(self): + self.execute("bench --site {site} db-console", kwargs={"cmd_input": rb"\q"}) + self.assertEqual(self.returncode, 0) diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index 002e1de8f2..5c792208b3 100644 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -429,6 +429,18 @@ jobs: name: Server services: + redis-cache: + image: redis:alpine + ports: + - 13000:6379 + redis-queue: + image: redis:alpine + ports: + - 11000:6379 + redis-socketio: + image: redis:alpine + ports: + - 12000:6379 mariadb: image: mariadb:10.6 env: @@ -439,17 +451,17 @@ jobs: steps: - name: Clone - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.10' - name: Setup Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: - node-version: 14 + node-version: 16 check-latest: true - name: Cache pip @@ -465,7 +477,7 @@ jobs: id: yarn-cache-dir-path run: 'echo "::set-output name=dir::$(yarn cache dir)"' - - uses: actions/cache@v2 + - uses: actions/cache@v3 id: yarn-cache with: path: ${{{{ steps.yarn-cache-dir-path.outputs.dir }}}}