Merge branch 'develop' into get_role_permissions-js-consistency
This commit is contained in:
commit
eec7a7fd13
48 changed files with 509 additions and 262 deletions
|
|
@ -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',]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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} |"
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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) || " ";
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
left: 0;
|
||||
margin: 0;
|
||||
padding: var(--padding-xs);
|
||||
z-index: 1;
|
||||
z-index: 4;
|
||||
min-width: 250px;
|
||||
|
||||
&> li {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@
|
|||
}
|
||||
|
||||
.page-head {
|
||||
z-index: 4;
|
||||
z-index: 6;
|
||||
position: sticky;
|
||||
top: var(--navbar-height);
|
||||
background: var(--bg-color);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -277,6 +277,7 @@ acceptable_elements = [
|
|||
"li",
|
||||
"m",
|
||||
"map",
|
||||
"mark",
|
||||
"menu",
|
||||
"meter",
|
||||
"multicol",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
138
socketio.js
138
socketio.js
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue