Merge branch 'develop' into get_role_permissions-js-consistency

This commit is contained in:
Marica 2022-11-21 14:03:17 +05:30 committed by GitHub
commit eec7a7fd13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 509 additions and 262 deletions

View file

@ -54,8 +54,8 @@ repos:
hooks:
- id: isort
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
hooks:
- id: flake8
additional_dependencies: ['flake8-bugbear',]

View file

@ -1432,6 +1432,8 @@ def get_doc_hooks():
@request_cache
def _load_app_hooks(app_name: str | None = None):
import types
hooks = {}
apps = [app_name] if app_name else get_installed_apps(sort=True)
@ -1447,9 +1449,13 @@ def _load_app_hooks(app_name: str | None = None):
if not request:
raise SystemExit
raise
for key in dir(app_hooks):
def _is_valid_hook(obj):
return not isinstance(obj, (types.ModuleType, types.FunctionType, type))
for key, value in inspect.getmembers(app_hooks, predicate=_is_valid_hook):
if not key.startswith("_"):
append_hook(hooks, key, getattr(app_hooks, key))
append_hook(hooks, key, value)
return hooks

View file

@ -21,7 +21,7 @@ from frappe import _
from frappe.auth import SAFE_HTTP_METHODS, UNSAFE_HTTP_METHODS, HTTPRequest
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
from frappe.middlewares import StaticDataMiddleware
from frappe.utils import get_site_name, sanitize_html
from frappe.utils import cint, get_site_name, sanitize_html
from frappe.utils.error import make_error_snapshot
from frappe.website.serve import get_response
@ -112,7 +112,7 @@ def init_request(request):
else:
frappe.connect(set_admin_as_user=False)
request.max_content_length = frappe.local.conf.get("max_file_size") or 10 * 1024 * 1024
request.max_content_length = cint(frappe.local.conf.get("max_file_size")) or 10 * 1024 * 1024
make_form_dict(request)

View file

@ -4,7 +4,6 @@ import os
import re
import shutil
import subprocess
from distutils.spawn import find_executable
from subprocess import getoutput
from tempfile import mkdtemp, mktemp
from urllib.parse import urlparse
@ -280,7 +279,7 @@ def check_node_executable():
warn = "⚠️ "
if node_version.major < 14:
click.echo(f"{warn} Please update your node version to 14")
if not find_executable("yarn"):
if not shutil.which("yarn"):
click.echo(f"{warn} Please install yarn using below command and try again.\nnpm install -g yarn")
click.echo()

View file

@ -2,7 +2,7 @@ import json
import os
import subprocess
import sys
from distutils.spawn import find_executable
from shutil import which
import click
@ -12,6 +12,7 @@ from frappe.coverage import CodeCoverage
from frappe.exceptions import SiteNotSpecifiedError
from frappe.utils import cint, update_progress_bar
find_executable = which # backwards compatibility
DATA_IMPORT_DEPRECATION = (
"[DEPRECATED] The `import-csv` command used 'Data Import Legacy' which has been deprecated.\n"
"Use `data-import` command instead to import data via 'Data Import'."
@ -525,7 +526,7 @@ def postgres(context):
def _mariadb():
from frappe.database.mariadb.database import MariaDBDatabase
mysql = find_executable("mysql")
mysql = which("mysql")
command = [
mysql,
"--port",
@ -544,7 +545,7 @@ def _mariadb():
def _psql():
psql = find_executable("psql")
psql = which("psql")
subprocess.run([psql, "-d", frappe.conf.db_name])

View file

@ -44,8 +44,10 @@ class Comment(Document):
return
frappe.publish_realtime(
f"update_docinfo_for_{self.reference_doctype}_{self.reference_name}",
"docinfo_update",
{"doc": self.as_dict(), "key": key, "action": action},
doctype=self.reference_doctype,
docname=self.reference_name,
after_commit=True,
)

View file

@ -233,8 +233,10 @@ class Communication(Document, CommunicationEmailMixin):
def notify_change(self, action):
frappe.publish_realtime(
f"update_docinfo_for_{self.reference_doctype}_{self.reference_name}",
"docinfo_update",
{"doc": self.as_dict(), "key": "communications", "action": action},
doctype=self.reference_doctype,
docname=self.reference_name,
after_commit=True,
)

View file

@ -139,6 +139,7 @@ class Importer:
"skipping": True,
"data_import": self.data_import.name,
},
user=frappe.session.user,
)
continue
@ -166,6 +167,7 @@ class Importer:
"row_indexes": row_indexes,
"eta": eta,
},
user=frappe.session.user,
)
create_import_log(

View file

@ -70,6 +70,7 @@
"columns",
"column_break_22",
"description",
"documentation_url",
"oldfieldname",
"oldfieldtype"
],
@ -541,13 +542,19 @@
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Virtual"
},
{
"depends_on": "eval:!in_list([\"Tab Break\", \"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
"fieldname": "documentation_url",
"fieldtype": "Small Text",
"label": "Documentation URL"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-04-19 12:27:28.641580",
"modified": "2022-11-17 14:14:39.404696",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",
@ -557,4 +564,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"states": []
}
}

View file

@ -78,21 +78,38 @@ class File(Document):
self.validate_duplicate_entry()
def validate(self):
if self.is_folder:
return
# Ensure correct formatting and type
self.file_url = unquote(self.file_url) if self.file_url else ""
self.validate_attachment_references()
# when dict is passed to get_doc for creation of new_doc, is_new returns None
# this case is handled inside handle_is_private_changed
if not self.is_new() and self.has_value_changed("is_private"):
self.handle_is_private_changed()
if not self.is_folder:
self.validate_file_path()
self.validate_file_url()
self.validate_file_on_disk()
self.validate_file_path()
self.validate_file_url()
self.validate_file_on_disk()
self.file_size = frappe.form_dict.file_size or self.file_size
def validate_attachment_references(self):
if not self.attached_to_doctype:
return
if not self.attached_to_name or not isinstance(self.attached_to_name, (str, int)):
frappe.throw(_("Attached To Name must be a string or an integer"), frappe.ValidationError)
if not self.attached_to_field:
return
if not frappe.get_meta(self.attached_to_doctype).has_field(self.attached_to_field):
frappe.throw(_("The fieldname you've specified in Attached To Field is invalid"))
def after_rename(self, *args, **kwargs):
for successor in self.get_successors():
setup_folder_path(successor, self.name)

View file

@ -85,7 +85,7 @@ class TestBase64File(FrappeTestCase):
"doctype": "File",
"file_name": "test_base64.txt",
"attached_to_doctype": self.attached_to_doctype,
"attached_to_docname": self.attached_to_docname,
"attached_to_name": self.attached_to_docname,
"content": self.test_content,
"decode": True,
}

View file

@ -18,11 +18,11 @@ class UserPermission(Document):
def on_update(self):
frappe.cache().hdel("user_permissions", self.user)
frappe.publish_realtime("update_user_permissions")
frappe.publish_realtime("update_user_permissions", user=self.user, after_commit=True)
def on_trash(self): # pylint: disable=no-self-use
def on_trash(self):
frappe.cache().hdel("user_permissions", self.user)
frappe.publish_realtime("update_user_permissions")
frappe.publish_realtime("update_user_permissions", user=self.user, after_commit=True)
def validate_user_permission(self):
"""checks for duplicate user permission records"""

View file

@ -51,12 +51,12 @@ class DbManager:
@staticmethod
def restore_database(target, source, user, password):
import os
from distutils.spawn import find_executable
from shutil import which
from frappe.utils import make_esc
esc = make_esc("$ ")
pv = find_executable("pv")
pv = which("pv")
if pv:
pipe = f"{pv} {source} |"

View file

@ -155,8 +155,6 @@ def clear_notifications(user=None):
else:
cache.delete_key("notification_count:" + name)
frappe.publish_realtime("clear_notifications")
def clear_notification_config(user):
frappe.cache().hdel("notification_config", user)
@ -164,7 +162,6 @@ def clear_notification_config(user):
def delete_notification_count_for(doctype):
frappe.cache().delete_key("notification_count:" + doctype)
frappe.publish_realtime("clear_notifications")
def clear_doctype_notifications(doc, method=None, *args, **kwargs):

View file

@ -206,11 +206,16 @@ def search_widget(
)
order_by = f"_relevance, {order_by}"
ptype = "select" if frappe.only_has_select_perm(doctype) else "read"
ignore_permissions = (
True
if doctype == "DocType"
else (cint(ignore_user_permissions) and has_permission(doctype, ptype=ptype))
else (
cint(ignore_user_permissions)
and has_permission(
doctype,
ptype="select" if frappe.only_has_select_perm(doctype) else "read",
)
)
)
values = frappe.get_list(

View file

@ -486,12 +486,6 @@ class EmailAccount(Document):
else:
frappe.db.commit()
# notify if user is linked to account
if len(inbound_mails) > 0 and not frappe.local.flags.in_test:
frappe.publish_realtime(
"new_email", {"account": self.email_account_name, "number": len(inbound_mails)}
)
if exceptions:
raise Exception(frappe.as_json(exceptions))

View file

@ -3,8 +3,12 @@
import json
import os
import re
import subprocess
import sys
from collections import OrderedDict
from contextlib import suppress
from shutil import which
import click
@ -653,10 +657,22 @@ def convert_archive_content(sql_file_path):
if frappe.conf.db_type == "mariadb":
# ever since mariaDB 10.6, row_format COMPRESSED has been deprecated and removed
# this step is added to ease restoring sites depending on older mariaDB servers
# This change was reverted by mariadb in 10.6.6
# Ref: https://mariadb.com/kb/en/innodb-compressed-row-format/#read-only
from pathlib import Path
from frappe.utils import random_string
version = _guess_mariadb_version()
if not version or (version <= (10, 6, 0) or version >= (10, 6, 6)):
return
click.secho(
"MariaDB version being used does not support ROW_FORMAT=COMPRESSED, "
"converting into DYNAMIC format.",
fg="yellow",
)
old_sql_file_path = Path(f"{sql_file_path}_{random_string(10)}")
sql_file_path = Path(sql_file_path)
@ -684,6 +700,20 @@ def extract_sql_gzip(sql_gz_path):
return decompressed_file
def _guess_mariadb_version() -> tuple[int] | None:
# Using command-line because we *might* not have a connection yet and this command is required
# in non-interactive mode.
# Use db.sql("select version()") instead if connection is available.
with suppress(Exception):
mysql = which("mysql")
version_output = subprocess.getoutput(f"{mysql} --version")
version_regex = r"(?P<version>\d+\.\d+\.\d+)-MariaDB"
version = re.search(version_regex, version_output).group("version")
return tuple(int(v) for v in version.split("."))
def extract_files(site_name, file_path):
import shutil
import subprocess

View file

@ -455,10 +455,10 @@ class DatabaseQuery:
)
def check_read_permission(self, doctype):
ptype = "select" if frappe.only_has_select_perm(doctype) else "read"
if not self.flags.ignore_permissions and not frappe.has_permission(
doctype, ptype=ptype, parent_doctype=self.doctype
doctype,
ptype="select" if frappe.only_has_select_perm(doctype) else "read",
parent_doctype=self.doctype,
):
frappe.flags.error_message = _("Insufficient Permission for {0}").format(frappe.bold(doctype))
raise frappe.PermissionError(doctype)

View file

@ -859,6 +859,12 @@
<path d="M15.3804 9.03564V10.0815H15.2573C14.6831 10.0903 14.2202 10.2397 13.8687 10.5298C13.52 10.8198 13.3105 11.2227 13.2402 11.7383C13.5801 11.3926 14.0093 11.2197 14.5278 11.2197C15.0845 11.2197 15.5269 11.4189 15.855 11.8174C16.1831 12.2158 16.3472 12.7402 16.3472 13.3906C16.3472 13.8066 16.2563 14.1831 16.0747 14.52C15.896 14.8569 15.6411 15.1191 15.3101 15.3066C14.9819 15.4941 14.6099 15.5879 14.1938 15.5879C13.52 15.5879 12.9751 15.3535 12.5591 14.8848C12.146 14.416 11.9395 13.7905 11.9395 13.0083V12.5513C11.9395 11.8569 12.0698 11.2446 12.3306 10.7144C12.5942 10.1812 12.9707 9.76953 13.46 9.47949C13.9521 9.18652 14.522 9.03857 15.1694 9.03564H15.3804ZM14.1411 12.2393C13.936 12.2393 13.75 12.2935 13.583 12.4019C13.416 12.5073 13.293 12.6479 13.2139 12.8237V13.2104C13.2139 13.6353 13.2974 13.9678 13.4644 14.208C13.6313 14.4453 13.8657 14.564 14.1675 14.564C14.4399 14.564 14.6597 14.457 14.8267 14.2432C14.9966 14.0264 15.0815 13.7466 15.0815 13.4038C15.0815 13.0552 14.9966 12.7739 14.8267 12.5601C14.6567 12.3462 14.4282 12.2393 14.1411 12.2393Z" fill="var(--icon-stroke)"/>
</symbol>
<symbol viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" stroke="none" id="icon-help">
<path stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M8.208 8.115v-.099a2.021 2.021 0 0 1 1.114-1.809l.057-.028a1.85 1.85 0 0 1 1.278-.142l.121.03c.623.156 1.1.659 1.223 1.289v0c.04.208.04.421 0 .63l-.029.153a1.805 1.805 0 0 1-.97 1.276l-.2.1a1.446 1.446 0 0 0-.804 1.297v0L10 11.5"/>
<path fill="#20272E" stroke="var(--icon-stroke)" d="M10.307 13.804a.304.304 0 1 1-.607 0 .304.304 0 0 1 .607 0Z"/>
<circle cx="10" cy="10" r="7" stroke="var(--icon-stroke)"/>
</symbol>
<symbol viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg" id="icon-edit-round">
<circle cx="13" cy="13" r="12.5" fill="#fff" stroke="var(--icon-stroke)"></circle>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.305 7.62c-.192.08-.367.197-.515.345l-.612.612 2.244 2.245.613-.612a1.586 1.586 0 0 0-1.73-2.59zm.41 3.91l-2.244-2.246-6.163 6.163a.5.5 0 0 0-.128.222l-.67 2.452a.3.3 0 0 0 .37.368l2.451-.669a.5.5 0 0 0 .222-.129l6.162-6.162z"

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View file

@ -13,18 +13,19 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
this.$wrapper = $('<div class="form-group frappe-control">').appendTo(this.parent);
} else {
this.$wrapper = $(
'<div class="frappe-control">\
<div class="form-group">\
<div class="clearfix">\
<label class="control-label" style="padding-right: 0px;"></label>\
</div>\
<div class="control-input-wrapper">\
<div class="control-input"></div>\
<div class="control-value like-disabled-input" style="display: none;"></div>\
<p class="help-box small text-muted"></p>\
</div>\
</div>\
</div>'
`<div class="frappe-control">
<div class="form-group">
<div class="clearfix">
<label class="control-label" style="padding-right: 0px;"></label>
<span class="ml-1 help"></span>
</div>
<div class="control-input-wrapper">
<div class="control-input"></div>
<div class="control-value like-disabled-input" style="display: none;"></div>
<p class="help-box small text-muted"></p>
</div>
</div>
</div>`
).appendTo(this.parent);
}
}
@ -79,7 +80,7 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
if (me.frm) {
me.value = frappe.model.get_value(me.doctype, me.docname, me.df.fieldname);
} else if (me.doc) {
me.value = me.doc[me.df.fieldname];
me.value = me.doc[me.df.fieldname] || "";
}
if (me.can_write()) {
@ -104,6 +105,7 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
me.set_description();
me.set_label();
me.set_doc_url();
me.set_mandatory(me.value);
me.set_bold();
me.set_required();
@ -141,6 +143,26 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
(icon ? '<i class="' + icon + '"></i> ' : "") + __(this.df.label) || "&nbsp;";
this._label = this.df.label;
}
set_doc_url() {
let unsupported_fieldtypes = frappe.model.no_value_type.filter(
(x) => frappe.model.table_fields.indexOf(x) === -1
);
if (
!this.df.label ||
!this.df?.documentation_url ||
in_list(unsupported_fieldtypes, this.df.fieldtype)
)
return;
let $help = this.$wrapper.find("span.help");
$help.empty();
$(`<a href="${this.df.documentation_url}" target="_blank">
${frappe.utils.icon("help", "sm")}
</a>`).appendTo($help);
}
set_description(description) {
if (description !== undefined) {
this.df.description = description;

View file

@ -8,6 +8,7 @@ frappe.ui.form.ControlCheck = class ControlCheck extends frappe.ui.form.ControlD
<span class="input-area"></span>
<span class="disp-area"></span>
<span class="label-area"></span>
<span class="ml-1 help"></span>
</label>
<p class="help-box small text-muted"></p>
</div>

View file

@ -8,6 +8,7 @@ frappe.ui.form.ControlSignature = class ControlSignature extends frappe.ui.form.
if (this.df.label) {
$(this.wrapper).find("label").text(__(this.df.label));
}
this.set_doc_url();
frappe.require("/assets/frappe/js/lib/jSignature.min.js").then(() => {
// make jSignature field

View file

@ -1750,7 +1750,7 @@ frappe.ui.form.Form = class FrappeForm {
if (this.meta.title_field) {
return this.doc[this.meta.title_field];
} else {
return this.doc.name;
return String(this.doc.name);
}
}
@ -1942,19 +1942,26 @@ frappe.ui.form.Form = class FrappeForm {
setup_docinfo_change_listener() {
let doctype = this.doctype;
let docname = this.docname;
let listener_name = `update_docinfo_for_${doctype}_${docname}`;
// to avoid duplicates
frappe.realtime.off(listener_name);
frappe.realtime.on(listener_name, ({ doc, key, action = "update" }) => {
let doc_list = frappe.model.docinfo[doctype][docname][key] || [];
if (action === "add") {
frappe.model.docinfo[doctype][docname][key].push(doc);
}
frappe.socketio.doc_subscribe(doctype, docname);
frappe.realtime.off("docinfo_update");
frappe.realtime.on("docinfo_update", ({ doc, key, action = "update" }) => {
if (
!doc.reference_doctype ||
!doc.reference_name ||
doc.reference_doctype !== doctype ||
doc.reference_name !== docname
) {
return;
}
let doc_list = frappe.model.docinfo[doctype][docname][key] || [];
let docindex = doc_list.findIndex((old_doc) => {
return old_doc.name === doc.name;
});
if (action === "add") {
frappe.model.docinfo[doctype][docname][key].push(doc);
}
if (docindex > -1) {
if (action === "update") {
frappe.model.docinfo[doctype][docname][key].splice(docindex, 1, doc);

View file

@ -62,6 +62,7 @@ export default class Grid {
make() {
let template = `
<label class="control-label">${__(this.df.label || "")}</label>
<span class="ml-1 help"></span>
<p class="text-muted small grid-description"></p>
<div class="grid-custom-buttons grid-field"></div>
<div class="form-grid-container">
@ -119,6 +120,7 @@ export default class Grid {
this.wrapper = $(template).appendTo(this.parent);
$(this.parent).addClass("form-group");
this.set_grid_description();
this.set_doc_url();
frappe.utils.bind_actions_with_object(this.wrapper, this);
@ -148,6 +150,26 @@ export default class Grid {
description_wrapper.hide();
}
}
set_doc_url() {
let unsupported_fieldtypes = frappe.model.no_value_type.filter(
(x) => frappe.model.table_fields.indexOf(x) === -1
);
if (
!this.df.label ||
!this.df?.documentation_url ||
in_list(unsupported_fieldtypes, this.df.fieldtype)
)
return;
let $help = $(this.parent).find("span.help");
$help.empty();
$(`<a href="${this.df.documentation_url}" target="_blank">
${frappe.utils.icon("help", "sm")}
</a>`).appendTo($help);
}
setup_grid_pagination() {
this.grid_pagination = new GridPagination({
grid: this,

View file

@ -649,13 +649,19 @@ export default class GridRow {
this.search_columns = {};
this.grid.setup_visible_columns();
let fields =
this.grid.user_defined_columns && this.grid.user_defined_columns.length > 0
? this.grid.user_defined_columns
: this.docfields;
this.grid.visible_columns.forEach((col, ci) => {
// to get update df for the row
let df = this.docfields.find((field) => field.fieldname === col[0].fieldname);
let df = fields.find((field) => field.fieldname === col[0].fieldname);
this.set_dependant_property(df);
let colsize = col[1];
let txt = this.doc
? frappe.format(this.doc[df.fieldname], df, null, this.doc)
: __(df.label);
@ -1348,7 +1354,12 @@ export default class GridRow {
}
}
refresh_field(fieldname, txt) {
let df = this.docfields.find((col) => {
let fields =
this.grid.user_defined_columns && this.grid.user_defined_columns.length > 0
? this.grid.user_defined_columns
: this.docfields;
let df = fields.find((col) => {
return col.fieldname === fieldname;
});

View file

@ -1315,7 +1315,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
if (this.list_view_settings && this.list_view_settings.disable_auto_refresh) {
return;
}
frappe.socketio.list_subscribe(this.doctype);
frappe.realtime.on("list_update", (data) => {
if (!frappe.get_doc(data?.doctype, data?.name)?.__unsaved) {
frappe.model.remove_from_locals(data.doctype, data.name);
}
if (this.avoid_realtime_update()) {
return;
}

View file

@ -12,6 +12,7 @@ frappe.route_history = [];
frappe.view_factory = {};
frappe.view_factories = [];
frappe.route_options = null;
frappe.open_in_new_tab = false;
frappe.route_hooks = {};
$(window).on("hashchange", function (e) {
@ -347,8 +348,13 @@ frappe.router = {
let sub_path = this.make_url(route);
// replace each # occurrences in the URL with encoded character except for last
// sub_path = sub_path.replace(/[#](?=.*[#])/g, "%23");
this.push_state(sub_path);
if (frappe.open_in_new_tab) {
localStorage["route_options"] = JSON.stringify(frappe.route_options);
window.open(sub_path, "_blank");
frappe.open_in_new_tab = false;
} else {
this.push_state(sub_path);
}
setTimeout(() => {
frappe.after_ajax &&
frappe.after_ajax(() => {
@ -493,6 +499,11 @@ frappe.router = {
frappe.route_options = {};
}
if (localStorage.getItem("route_options")) {
frappe.route_options = JSON.parse(localStorage.getItem("route_options"));
localStorage.removeItem("route_options");
}
let params = new URLSearchParams(query_string);
for (const [key, value] of params) {
frappe.route_options[key] = value;

View file

@ -3,6 +3,7 @@ frappe.socketio = {
open_tasks: {},
open_docs: [],
emit_queue: [],
init: function (port = 3000) {
if (frappe.boot.disable_async) {
return;
@ -17,14 +18,12 @@ frappe.socketio = {
frappe.socketio.socket = io.connect(frappe.socketio.get_host(port), {
secure: true,
withCredentials: true,
reconnectionAttempts: 3,
});
} else if (window.location.protocol == "http:") {
frappe.socketio.socket = io.connect(frappe.socketio.get_host(port), {
withCredentials: true,
});
} else if (window.location.protocol == "file:") {
frappe.socketio.socket = io.connect(window.localStorage.server, {
withCredentials: true,
reconnectionAttempts: 3,
});
}
@ -130,6 +129,9 @@ frappe.socketio = {
task_unsubscribe: function (task_id) {
frappe.socketio.socket.emit("task_unsubscribe", task_id);
},
list_subscribe: function (doctype) {
frappe.socketio.socket.emit("list_update", doctype);
},
doc_subscribe: function (doctype, docname) {
if (frappe.flags.doc_subscribe) {
console.log("throttled");

View file

@ -463,7 +463,12 @@ frappe.ui.Page = class Page {
`);
}
$link = $li.find("a").on("click", click);
$link = $li.find("a").on("click", (e) => {
if (e.ctrlKey || e.metaKey) {
frappe.open_in_new_tab = true;
}
return click();
});
if (standard) {
$li.appendTo(parent);

View file

@ -107,6 +107,10 @@ frappe.search.AwesomeBar = class AwesomeBar {
if (item.onclick) {
item.onclick(item.match);
} else {
let event = o.originalEvent;
if (event.ctrlKey || event.metaKey) {
frappe.open_in_new_tab = true;
}
frappe.set_route(item.route);
}
$input.val("");

View file

@ -56,6 +56,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
if (this.list_view_settings?.disable_auto_refresh) {
return;
}
frappe.socketio.list_subscribe(this.doctype);
frappe.realtime.on("list_update", (data) => this.on_update(data));
}

View file

@ -55,7 +55,7 @@ frappe.ready(function () {
function setup_fields(web_form_doc, doc_data) {
web_form_doc.web_form_fields.forEach((df) => {
df.is_web_form = true;
df.read_only = !web_form_doc.is_new && !web_form_doc.in_edit_mode;
df.read_only = df.read_only || (!web_form_doc.is_new && !web_form_doc.in_edit_mode);
if (df.fieldtype === "Table") {
df.get_data = () => {
let data = [];

View file

@ -161,7 +161,10 @@ export default class QuickListWidget extends Widget {
$quick_list_item
);
$quick_list_item.click(() => {
$quick_list_item.click((e) => {
if (e.ctrlKey || e.metaKey) {
frappe.open_in_new_tab = true;
}
frappe.set_route(`${frappe.utils.get_form_link(this.document_type, doc.name)}`);
});
@ -243,7 +246,14 @@ export default class QuickListWidget extends Widget {
}
let route = frappe.utils.generate_route({ type: "doctype", name: this.document_type });
this.see_all_button = $(`
<a href="${route}"class="see-all btn btn-xs">${__("View List")}</a>
<div class="see-all btn btn-xs">${__("View List")}</div>
`).appendTo(this.footer);
this.see_all_button.click((e) => {
if (e.ctrlKey || e.metaKey) {
frappe.open_in_new_tab = true;
}
frappe.set_route(route);
});
}
}

View file

@ -24,7 +24,7 @@ export default class ShortcutWidget extends Widget {
}
setup_events() {
this.widget.click(() => {
this.widget.click((e) => {
if (this.in_customize_mode) return;
let route = frappe.utils.generate_route({
@ -40,6 +40,11 @@ export default class ShortcutWidget extends Widget {
if (this.type == "DocType" && filters) {
frappe.route_options = filters;
}
if (e.ctrlKey || e.metaKey) {
frappe.open_in_new_tab = true;
}
frappe.set_route(route);
});
}

View file

@ -30,7 +30,7 @@
left: 0;
margin: 0;
padding: var(--padding-xs);
z-index: 1;
z-index: 4;
min-width: 250px;
&> li {

View file

@ -11,7 +11,7 @@
width: 100%;
.modal-actions {
z-index: 4;
z-index: 6;
top: 7px;
}
@ -31,7 +31,7 @@
.search-icon {
position: absolute;
top: 15px;
z-index: 4;
z-index: 6;
}
}
.search-results {

View file

@ -84,7 +84,7 @@
}
.page-head {
z-index: 4;
z-index: 6;
position: sticky;
top: var(--navbar-height);
background: var(--bg-color);

View file

@ -2,6 +2,7 @@
# License: MIT. See LICENSE
import os
from contextlib import suppress
import redis
@ -22,14 +23,14 @@ def publish_progress(percent, title=None, doctype=None, docname=None, descriptio
def publish_realtime(
event=None,
message=None,
room=None,
user=None,
doctype=None,
docname=None,
task_id=None,
after_commit=False,
event: str = None,
message: dict = None,
room: str = None,
user: str = None,
doctype: str = None,
docname: str = None,
task_id: str = None,
after_commit: bool = False,
):
"""Publish real-time updates
@ -44,29 +45,31 @@ def publish_realtime(
message = {}
if event is None:
if getattr(frappe.local, "task_id", None):
event = "task_progress"
else:
event = "global"
if event == "msgprint" and not user:
event = "task_progress" if frappe.local.task_id else "global"
elif event == "msgprint" and not user:
user = frappe.session.user
elif event == "list_update":
doctype = doctype or message.get("doctype")
room = get_doctype_room(doctype)
elif event == "docinfo_update":
room = get_doc_room(doctype, docname)
if not task_id and hasattr(frappe.local, "task_id"):
task_id = frappe.local.task_id
if not room:
if not task_id and hasattr(frappe.local, "task_id"):
task_id = frappe.local.task_id
if task_id:
room = get_task_progress_room(task_id)
if not "task_id" in message:
message["task_id"] = task_id
after_commit = False
if "task_id" not in message:
message["task_id"] = task_id
room = get_task_progress_room(task_id)
elif user:
# transmit to specific user: System, Website or Guest
room = get_user_room(user)
elif doctype and docname:
room = get_doc_room(doctype, docname)
else:
# This will be broadcasted to all Desk users
room = get_site_room()
if after_commit:
@ -83,13 +86,10 @@ def emit_via_redis(event, message, room):
:param event: Event name, like `task_progress` etc.
:param message: JSON message object. For async must contain `task_id`
:param room: name of the room"""
r = get_redis_server()
try:
with suppress(redis.exceptions.ConnectionError):
r = get_redis_server()
r.publish("events", frappe.as_json({"event": event, "message": message, "room": room}))
except redis.exceptions.ConnectionError:
# print(frappe.get_traceback())
pass
def get_redis_server():
@ -117,27 +117,47 @@ def can_subscribe_doc(doctype, docname):
return True
@frappe.whitelist(allow_guest=True)
def can_subscribe_list(doctype):
from frappe.exceptions import PermissionError
if not frappe.has_permission(user=frappe.session.user, doctype=doctype, ptype="read"):
raise PermissionError()
return True
@frappe.whitelist(allow_guest=True)
def get_user_info():
from frappe.sessions import Session
session = Session(None, resume=True).get_session_data()
return {
"user": session.user,
"user_type": session.user_type,
}
def get_doctype_room(doctype):
return f"{frappe.local.site}:doctype:{doctype}"
def get_doc_room(doctype, docname):
return "".join([frappe.local.site, ":doc:", doctype, "/", cstr(docname)])
return f"{frappe.local.site}:doc:{doctype}/{cstr(docname)}"
def get_user_room(user):
return "".join([frappe.local.site, ":user:", user])
return f"{frappe.local.site}:user:{user}"
def get_site_room():
return "".join([frappe.local.site, ":all"])
return f"{frappe.local.site}:all"
def get_task_progress_room(task_id):
return "".join([frappe.local.site, ":task_progress:", task_id])
return f"{frappe.local.site}:task_progress:{task_id}"
def get_website_room():
return f"{frappe.local.site}:website"

View file

@ -101,7 +101,9 @@ class Recorder:
}
frappe.cache().hset(RECORDER_REQUEST_SPARSE_HASH, self.uuid, request_data)
frappe.publish_realtime(
event="recorder-dump-event", message=json.dumps(request_data, default=str)
event="recorder-dump-event",
message=json.dumps(request_data, default=str),
user="Administrator",
)
self.mark_duplicates()

View file

@ -34,10 +34,11 @@ class EnergyPointLog(Document):
def after_insert(self):
alert_dict = get_alert_dict(self)
if alert_dict:
frappe.publish_realtime("energy_point_alert", message=alert_dict, user=self.user)
frappe.publish_realtime(
"energy_point_alert", message=alert_dict, user=self.user, after_commit=True
)
frappe.cache().hdel("energy_points", self.user)
frappe.publish_realtime("update_points", after_commit=True)
if self.type != "Review" and frappe.get_cached_value(
"Notification Settings", self.user, "energy_points_system_notifications"

View file

@ -67,24 +67,41 @@ class TestWebsite(FrappeTestCase):
self.assertEqual(get_home_page(), "login")
frappe.set_user("Administrator")
from frappe import get_hooks
def patched_get_hooks(hook, value):
def wrapper(*args, **kwargs):
return_value = get_hooks(*args, **kwargs)
if args[0] == hook:
return_value = value
return return_value
return wrapper
# test homepage via hooks
clear_website_cache()
set_home_page_hook(
"get_website_user_home_page", "frappe.www._test._test_home_page.get_website_user_home_page"
)
self.assertEqual(get_home_page(), "_test/_test_folder")
with patch.object(
frappe,
"get_hooks",
patched_get_hooks(
"get_website_user_home_page", ["frappe.www._test._test_home_page.get_website_user_home_page"]
),
):
self.assertEqual(get_home_page(), "_test/_test_folder")
clear_website_cache()
set_home_page_hook("website_user_home_page", "login")
self.assertEqual(get_home_page(), "login")
with patch.object(frappe, "get_hooks", patched_get_hooks("website_user_home_page", ["login"])):
self.assertEqual(get_home_page(), "login")
clear_website_cache()
set_home_page_hook("home_page", "about")
self.assertEqual(get_home_page(), "about")
with patch.object(frappe, "get_hooks", patched_get_hooks("home_page", ["about"])):
self.assertEqual(get_home_page(), "about")
clear_website_cache()
set_home_page_hook("role_home_page", {"home-page-test": "home-page-test"})
self.assertEqual(get_home_page(), "home-page-test")
with patch.object(
frappe, "get_hooks", patched_get_hooks("role_home_page", {"home-page-test": ["home-page-test"]})
):
self.assertEqual(get_home_page(), "home-page-test")
def test_page_load(self):
set_request(method="POST", path="login")
@ -196,24 +213,26 @@ class TestWebsite(FrappeTestCase):
frappe.cache().delete_key("app_hooks")
def test_custom_page_renderer(self):
import frappe.hooks
from frappe import get_hooks
frappe.hooks.page_renderer = ["frappe.tests.test_website.CustomPageRenderer"]
frappe.cache().delete_key("app_hooks")
set_request(method="GET", path="/custom")
response = get_response()
self.assertEqual(response.status_code, 3984)
def patched_get_hooks(*args, **kwargs):
return_value = get_hooks(*args, **kwargs)
if args and args[0] == "page_renderer":
return_value = ["frappe.tests.test_website.CustomPageRenderer"]
return return_value
set_request(method="GET", path="/new")
content = get_response_content()
self.assertIn("<div>Custom Page Response</div>", content)
with patch.object(frappe, "get_hooks", patched_get_hooks):
set_request(method="GET", path="/custom")
response = get_response()
self.assertEqual(response.status_code, 3984)
set_request(method="GET", path="/random")
response = get_response()
self.assertEqual(response.status_code, 404)
set_request(method="GET", path="/new")
content = get_response_content()
self.assertIn("<div>Custom Page Response</div>", content)
delattr(frappe.hooks, "page_renderer")
frappe.cache().delete_key("app_hooks")
set_request(method="GET", path="/random")
response = get_response()
self.assertEqual(response.status_code, 404)
def test_printview_page(self):
frappe.db.value_cache[("DocType", "Language", "name")] = (("Language",),)
@ -333,22 +352,35 @@ class TestWebsite(FrappeTestCase):
frappe.render_template(content, context), '<a class="btn btn-default btn-primary">Test</a>'
)
def test_app_include(self):
from frappe import get_hooks
def set_home_page_hook(key, value):
from frappe import hooks
def patched_get_hooks(*args, **kwargs):
return_value = get_hooks(*args, **kwargs)
if isinstance(return_value, dict) and "app_include_js" in return_value:
return_value.app_include_js.append("test_app_include.js")
return_value.app_include_css.append("test_app_include.css")
return return_value
# reset home_page hooks
for hook in (
"get_website_user_home_page",
"website_user_home_page",
"role_home_page",
"home_page",
):
if hasattr(hooks, hook):
delattr(hooks, hook)
with patch.object(frappe, "get_hooks", patched_get_hooks):
frappe.set_user("Administrator")
frappe.hooks.app_include_js.append("test_app_include.js")
frappe.hooks.app_include_css.append("test_app_include.css")
frappe.conf.update({"app_include_js": ["test_app_include_via_site_config.js"]})
frappe.conf.update({"app_include_css": ["test_app_include_via_site_config.css"]})
setattr(hooks, key, value)
frappe.cache().delete_key("app_hooks")
set_request(method="GET", path="/app")
content = get_response_content("/app")
self.assertIn('<script type="text/javascript" src="/test_app_include.js"></script>', content)
self.assertIn(
'<script type="text/javascript" src="/test_app_include_via_site_config.js"></script>', content
)
self.assertIn('<link type="text/css" rel="stylesheet" href="/test_app_include.css">', content)
self.assertIn(
'<link type="text/css" rel="stylesheet" href="/test_app_include_via_site_config.css">', content
)
delattr(frappe.local, "request")
frappe.set_user("Guest")
class CustomPageRenderer:

View file

@ -1,27 +0,0 @@
""" smoak tests to check that all registered background jobs execute without error.
Note: Filename is intentional to run this test roughly at end. Don't change."""
import time
import frappe
from frappe.core.doctype.rq_job.rq_job import RQJob, remove_failed_jobs
from frappe.tests.utils import FrappeTestCase, timeout
class TestScheduledJobSanity(FrappeTestCase):
def setUp(self):
remove_failed_jobs()
@timeout(90)
def test_bg_jobs_run(self):
"""Enqueue all scheduled jobs, wait for finish and verify that none failed."""
for scheduled_job_type in frappe.get_all("Scheduled Job Type", pluck="name"):
frappe.get_doc("Scheduled Job Type", scheduled_job_type).enqueue(force=True)
while RQJob.get_list({"filters": [["RQ Job", "status", "in", ("queued", "started")]]}):
time.sleep(0.5)
# Check no failed, if failed print full details
failed_jobs = RQJob.get_list({"filters": [["RQ Job", "status", "=", "failed"]]})
self.assertEqual(len(failed_jobs), 0, "Jobs failed: " + str(failed_jobs))

View file

@ -5,7 +5,14 @@ from frappe.utils import add_to_date, now
UI_TEST_USER = "frappe@example.com"
@frappe.whitelist()
def whitelist_for_tests(fn):
if frappe.request and not (frappe.flags.in_test or getattr(frappe.local, "dev_server", 0)):
frappe.throw("Cannot run UI tests. Use a development server with `bench start`")
return frappe.whitelist()(fn)
@whitelist_for_tests
def create_if_not_exists(doc):
"""Create records if they dont exist.
Will check for uniqueness by checking if a record exists with these field value pairs
@ -13,9 +20,6 @@ def create_if_not_exists(doc):
:param doc: dict of field value pairs. can be a list of dict for multiple records.
"""
if not frappe.local.dev_server:
frappe.throw(_("This method can only be accessed in development"), frappe.PermissionError)
doc = frappe.parse_json(doc)
if not isinstance(doc, list):
@ -38,7 +42,7 @@ def create_if_not_exists(doc):
return names
@frappe.whitelist()
@whitelist_for_tests
def create_todo_records():
frappe.db.truncate("ToDo")
@ -72,16 +76,13 @@ def create_todo_records():
).insert()
@frappe.whitelist()
@whitelist_for_tests
def clear_notes():
if not frappe.local.dev_server:
frappe.throw(_("Not allowed"), frappe.PermissionError)
for note in frappe.get_all("Note", pluck="name"):
frappe.delete_doc("Note", note, force=True)
@frappe.whitelist()
@whitelist_for_tests
def create_communication_record():
doc = frappe.get_doc(
{
@ -95,7 +96,7 @@ def create_communication_record():
return doc
@frappe.whitelist()
@whitelist_for_tests
def setup_workflow():
from frappe.workflow.doctype.workflow.test_workflow import create_todo_workflow
@ -104,7 +105,7 @@ def setup_workflow():
frappe.clear_cache()
@frappe.whitelist()
@whitelist_for_tests
def create_contact_phone_nos_records():
if frappe.get_all("Contact", {"first_name": "Test Contact"}):
return
@ -116,7 +117,7 @@ def create_contact_phone_nos_records():
doc.insert()
@frappe.whitelist()
@whitelist_for_tests
def create_doctype(name, fields):
fields = frappe.parse_json(fields)
if frappe.db.exists("DocType", name):
@ -133,7 +134,7 @@ def create_doctype(name, fields):
).insert()
@frappe.whitelist()
@whitelist_for_tests
def create_child_doctype(name, fields):
fields = frappe.parse_json(fields)
if frappe.db.exists("DocType", name):
@ -151,7 +152,7 @@ def create_child_doctype(name, fields):
).insert()
@frappe.whitelist()
@whitelist_for_tests
def create_contact_records():
if frappe.get_all("Contact", {"first_name": "Test Form Contact 1"}):
return
@ -161,7 +162,7 @@ def create_contact_records():
insert_contact("Test Form Contact 3", "12345")
@frappe.whitelist()
@whitelist_for_tests
def create_multiple_todo_records():
if frappe.get_all("ToDo", {"description": "Multiple ToDo 1"}):
return
@ -177,7 +178,7 @@ def insert_contact(first_name, phone_number):
doc.insert()
@frappe.whitelist()
@whitelist_for_tests
def create_form_tour():
if frappe.db.exists("Form Tour", {"name": "Test Form Tour"}):
return
@ -227,7 +228,7 @@ def create_form_tour():
tour.insert()
@frappe.whitelist()
@whitelist_for_tests
def create_data_for_discussions():
web_page = create_web_page("Test page for discussions", "test-page-discussions", False)
create_topic_and_reply(web_page)
@ -285,7 +286,7 @@ def create_topic_and_reply(web_page):
reply.save()
@frappe.whitelist()
@whitelist_for_tests
def update_webform_to_multistep():
if not frappe.db.exists("Web Form", "update-profile-duplicate"):
doc = frappe.get_doc("Web Form", "edit-profile")
@ -297,7 +298,7 @@ def update_webform_to_multistep():
_doc.save()
@frappe.whitelist()
@whitelist_for_tests
def update_child_table(name):
doc = frappe.get_doc("DocType", name)
if len(doc.fields) == 1:
@ -315,7 +316,7 @@ def update_child_table(name):
doc.save()
@frappe.whitelist()
@whitelist_for_tests
def insert_doctype_with_child_table_record(name):
if frappe.get_all(name, {"title": "Test Grid Search"}):
return
@ -361,7 +362,7 @@ def insert_doctype_with_child_table_record(name):
doc.insert()
@frappe.whitelist()
@whitelist_for_tests
def insert_translations():
translation = [
{
@ -395,7 +396,7 @@ def insert_translations():
frappe.get_doc(doc).insert()
@frappe.whitelist()
@whitelist_for_tests
def create_blog_post():
blog_category = frappe.get_doc(
@ -449,7 +450,7 @@ def create_test_user():
user.save()
@frappe.whitelist()
@whitelist_for_tests
def setup_tree_doctype():
frappe.delete_doc_if_exists("DocType", "Custom Tree", force=True)
@ -473,7 +474,7 @@ def setup_tree_doctype():
frappe.get_doc({"doctype": "Custom Tree", "tree": "All Trees"}).insert()
@frappe.whitelist()
@whitelist_for_tests
def setup_image_doctype():
frappe.delete_doc_if_exists("DocType", "Custom Image", force=True)
@ -492,7 +493,7 @@ def setup_image_doctype():
).insert()
@frappe.whitelist()
@whitelist_for_tests
def setup_inbox():
frappe.db.sql("DELETE FROM `tabUser Email`")
@ -501,7 +502,7 @@ def setup_inbox():
user.save()
@frappe.whitelist()
@whitelist_for_tests
def setup_default_view(view, force_reroute=None):
frappe.delete_doc_if_exists("Property Setter", "Event-main-default_view")
frappe.delete_doc_if_exists("Property Setter", "Event-main-force_re_route_to_default_view")
@ -532,13 +533,13 @@ def setup_default_view(view, force_reroute=None):
).insert()
@frappe.whitelist()
@whitelist_for_tests
def create_note():
if not frappe.db.exists("Note", "Routing Test"):
frappe.get_doc({"doctype": "Note", "title": "Routing Test"}).insert()
@frappe.whitelist()
@whitelist_for_tests
def create_kanban():
if not frappe.db.exists("Custom Field", "Note-kanban"):
frappe.get_doc(

View file

@ -277,6 +277,7 @@ acceptable_elements = [
"li",
"m",
"map",
"mark",
"menu",
"meter",
"multicol",

View file

@ -3,12 +3,14 @@
import frappe
from frappe.model.document import Document
from frappe.realtime import get_website_room
class DiscussionReply(Document):
def on_update(self):
frappe.publish_realtime(
event="update_message",
room=get_website_room(),
message={"reply": frappe.utils.md_to_html(self.reply), "reply_name": self.name},
after_commit=True,
)
@ -41,6 +43,7 @@ class DiscussionReply(Document):
frappe.publish_realtime(
event="publish_message",
room=get_website_room(),
message={
"template": template,
"topic_info": topic_info[0],
@ -53,10 +56,15 @@ class DiscussionReply(Document):
def after_delete(self):
frappe.publish_realtime(
event="delete_message", message={"reply_name": self.name}, after_commit=True
event="delete_message",
room=get_website_room(),
message={"reply_name": self.name},
after_commit=True,
)
@frappe.whitelist()
def delete_message(reply_name):
frappe.delete_doc("Discussion Reply", reply_name, ignore_permissions=True)
owner = frappe.db.get_value("Discussion Reply", reply_name, "owner")
if owner == frappe.session.user:
frappe.delete_doc("Discussion Reply", reply_name)

View file

@ -44,12 +44,15 @@ def get_context(context):
boot_json = CLOSING_SCRIPT_TAG_PATTERN.sub("", boot_json)
boot_json = json.dumps(boot_json)
include_js = hooks.get("app_include_js", []) + frappe.conf.get("app_include_js", [])
include_css = hooks.get("app_include_css", []) + frappe.conf.get("app_include_css", [])
context.update(
{
"no_cache": 1,
"build_version": frappe.utils.get_build_version(),
"include_js": hooks["app_include_js"],
"include_css": hooks["app_include_css"],
"include_js": include_js,
"include_css": include_css,
"layout_direction": "rtl" if is_rtl() else "ltr",
"lang": frappe.local.lang,
"sounds": hooks["sounds"],

View file

@ -48,6 +48,7 @@ dependencies = [
"premailer~=3.8.0",
"psutil~=5.9.1",
"psycopg2-binary~=2.9.1",
"pyOpenSSL~=22.1.0",
"pycryptodome~=3.10.1",
"pyotp~=2.6.0",
"python-dateutil~=2.8.1",

View file

@ -14,47 +14,59 @@ const io = require("socket.io")(conf.socketio_port, {
},
});
// on socket connection
io.on("connection", function (socket) {
io.use((socket, next) => {
if (get_hostname(socket.request.headers.host) != get_hostname(socket.request.headers.origin)) {
next(new Error("Invalid origin"));
return;
}
if (!socket.request.headers.cookie) {
next(new Error("No cookie transmitted."));
return;
}
const sid = cookie.parse(socket.request.headers.cookie).sid;
if (!sid) {
let cookies = cookie.parse(socket.request.headers.cookie);
if (!cookies.sid) {
next(new Error("No sid transmitted."));
return;
}
socket.user = cookie.parse(socket.request.headers.cookie).user_id;
request
.get(get_url(socket, "/api/method/frappe.realtime.get_user_info"))
.type("form")
.query({
sid: cookies.sid,
})
.then((res) => {
socket.user = res.body.message.user;
socket.user_type = res.body.message.user_type;
socket.sid = cookies.sid;
next();
})
.catch((e) => {
next(new Error(`Unauthorized: ${e}`));
});
});
let retries = 0;
let join_user_room = () => {
request
.get(get_url(socket, "/api/method/frappe.realtime.get_user_info"))
.type("form")
.query({
sid: sid,
})
.then((res) => {
const room = get_user_room(socket, res.body.message.user);
socket.join(room);
socket.join(get_site_room(socket));
})
.catch((e) => {
if (e.code === "ECONNREFUSED" && retries < 5) {
// retry after 1s
retries += 1;
return setTimeout(join_user_room, 1000);
}
log(`Unable to join user room. ${e}`);
});
};
// on socket connection
io.on("connection", function (socket) {
socket.join(get_user_room(socket, socket.user));
socket.join(get_website_room(socket));
join_user_room();
if (socket.user_type == "System User") {
socket.join(get_site_room(socket));
}
socket.on("list_update", function (doctype) {
can_subscribe_list({
socket,
doctype,
callback: () => {
socket.join(get_doctype_room(socket, doctype));
},
});
});
socket.on("task_subscribe", function (task_id) {
var room = get_task_room(socket, task_id);
@ -69,13 +81,11 @@ io.on("connection", function (socket) {
socket.on("progress_subscribe", function (task_id) {
var room = get_task_room(socket, task_id);
socket.join(room);
send_existing_lines(task_id, socket);
});
socket.on("doc_subscribe", function (doctype, docname) {
can_subscribe_doc({
socket,
sid,
doctype,
docname,
callback: () => {
@ -93,7 +103,6 @@ io.on("connection", function (socket) {
socket.on("doc_open", function (doctype, docname) {
can_subscribe_doc({
socket,
sid,
doctype,
docname,
callback: () => {
@ -185,18 +194,6 @@ subscriber.on("message", function (_channel, message) {
subscriber.subscribe("events");
function send_existing_lines(task_id, socket) {
var room = get_task_room(socket, task_id);
subscriber.hgetall("task_log:" + task_id, function (_err, lines) {
io.to(room).emit("task_progress", {
task_id: task_id,
message: {
lines: lines,
},
});
});
}
function get_doc_room(socket, doctype, docname) {
return get_site_name(socket) + ":doc:" + doctype + "/" + docname;
}
@ -210,33 +207,42 @@ function get_typing_room(socket, doctype, docname) {
}
function get_user_room(socket, user) {
return get_site_name(socket) + ":user:" + user;
return get_site_name(socket) + ":user:" + user || socket.user;
}
function get_site_room(socket) {
return get_site_name(socket) + ":all";
}
function get_website_room(socket) {
return get_site_name(socket) + ":website";
}
function get_doctype_room(socket, doctype) {
return get_site_name(socket) + ":doctype:" + doctype;
}
function get_task_room(socket, task_id) {
return get_site_name(socket) + ":task_progress:" + task_id;
}
function get_site_name(socket) {
var hostname_from_host = get_hostname(socket.request.headers.host);
if (socket.request.headers["x-frappe-site-name"]) {
return get_hostname(socket.request.headers["x-frappe-site-name"]);
if (socket.site_name) {
return socket.site_name;
} else if (socket.request.headers["x-frappe-site-name"]) {
socket.site_name = get_hostname(socket.request.headers["x-frappe-site-name"]);
} else if (
["localhost", "127.0.0.1"].indexOf(hostname_from_host) !== -1 &&
conf.default_site
conf.default_site &&
["localhost", "127.0.0.1"].indexOf(get_hostname(socket.request.headers.host)) !== -1
) {
// from currentsite.txt since host is localhost
return conf.default_site;
socket.site_name = conf.default_site;
} else if (socket.request.headers.origin) {
return get_hostname(socket.request.headers.origin);
socket.site_name = get_hostname(socket.request.headers.origin);
} else {
return get_hostname(socket.request.headers.host);
socket.site_name = get_hostname(socket.request.headers.host);
}
return socket.site_name;
}
function get_hostname(url) {
@ -261,7 +267,7 @@ function can_subscribe_doc(args) {
.get(get_url(args.socket, "/api/method/frappe.realtime.can_subscribe_doc"))
.type("form")
.query({
sid: args.sid,
sid: args.socket.sid,
doctype: args.doctype,
docname: args.docname,
})
@ -280,6 +286,30 @@ function can_subscribe_doc(args) {
});
}
function can_subscribe_list(args) {
if (!args) return;
if (!args.doctype) return;
request
.get(get_url(args.socket, "/api/method/frappe.realtime.can_subscribe_list"))
.type("form")
.query({
sid: args.socket.sid,
doctype: args.doctype,
})
.end(function (err, res) {
if (!res || res.status == 403 || err) {
if (err) {
log(err);
}
return false;
} else if (res.status == 200) {
args?.callback(err, res);
return true;
}
log("ERROR (can_subscribe_list): ", err, res);
});
}
function send_users(args, action) {
if (!(args && args.doctype && args.docname)) {
return;