@@ -16,3 +16,6 @@ fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85 | |||||
# Refactor "not a in b" -> "a not in b" | # Refactor "not a in b" -> "a not in b" | ||||
745297a49d516e5e3c4bb3e1b0c4235e7d31165d | 745297a49d516e5e3c4bb3e1b0c4235e7d31165d | ||||
# Clean up whitespace | |||||
b2fc959307c7c79f5584625569d5aed04133ba13 |
@@ -1,15 +1,24 @@ | |||||
name: Semgrep | |||||
name: Linters | |||||
on: | on: | ||||
pull_request: { } | pull_request: { } | ||||
jobs: | jobs: | ||||
semgrep: | |||||
linters: | |||||
name: Frappe Linter | name: Frappe Linter | ||||
runs-on: ubuntu-latest | runs-on: ubuntu-latest | ||||
steps: | steps: | ||||
- uses: actions/checkout@v2 | - uses: actions/checkout@v2 | ||||
- name: Set up Python 3.8 | |||||
uses: actions/setup-python@v2 | |||||
with: | |||||
python-version: 3.8 | |||||
- name: Install and Run Pre-commit | |||||
uses: pre-commit/action@v2.0.3 | |||||
- name: Download Semgrep rules | - name: Download Semgrep rules | ||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules | run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules | ||||
@@ -0,0 +1,23 @@ | |||||
exclude: 'node_modules|.git' | |||||
default_stages: [commit] | |||||
fail_fast: false | |||||
repos: | |||||
- repo: https://github.com/pre-commit/pre-commit-hooks | |||||
rev: v4.0.1 | |||||
hooks: | |||||
- id: trailing-whitespace | |||||
files: "frappe.*" | |||||
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" | |||||
- id: check-yaml | |||||
- id: no-commit-to-branch | |||||
args: ['--branch', 'develop'] | |||||
- id: check-merge-conflict | |||||
- id: check-ast | |||||
ci: | |||||
autoupdate_schedule: weekly | |||||
skip: [] | |||||
submodules: false |
@@ -3,7 +3,6 @@ codecov: | |||||
coverage: | coverage: | ||||
status: | status: | ||||
patch: off | |||||
project: | project: | ||||
default: false | default: false | ||||
server: | server: | ||||
@@ -166,7 +166,7 @@ class Importer: | |||||
if not self.data_import.status == "Partial Success": | if not self.data_import.status == "Partial Success": | ||||
self.data_import.db_set("status", "Partial Success") | self.data_import.db_set("status", "Partial Success") | ||||
# commit after every successful import | # commit after every successful import | ||||
frappe.db.commit() | frappe.db.commit() | ||||
@@ -2,7 +2,8 @@ | |||||
# See license.txt | # See license.txt | ||||
# import frappe | # import frappe | ||||
import unittest | |||||
from frappe.tests.utils import FrappeTestCase | |||||
class Test{classname}(unittest.TestCase): | |||||
class Test{classname}(FrappeTestCase): | |||||
pass | pass |
@@ -314,19 +314,19 @@ result = [ | |||||
{ | { | ||||
"parent_column": "Parent 1", | "parent_column": "Parent 1", | ||||
"column_1": 200, | "column_1": 200, | ||||
"column_2": 150.50 | |||||
"column_2": 150.50 | |||||
}, | }, | ||||
{ | { | ||||
"parent_column": "Child 1", | "parent_column": "Child 1", | ||||
"column_1": 100, | "column_1": 100, | ||||
"column_2": 75.25, | "column_2": 75.25, | ||||
"parent_value": "Parent 1" | |||||
"parent_value": "Parent 1" | |||||
}, | }, | ||||
{ | { | ||||
"parent_column": "Child 2", | "parent_column": "Child 2", | ||||
"column_1": 100, | "column_1": 100, | ||||
"column_2": 75.25, | "column_2": 75.25, | ||||
"parent_value": "Parent 1" | |||||
"parent_value": "Parent 1" | |||||
} | } | ||||
] | ] | ||||
@@ -244,7 +244,13 @@ class Query: | |||||
_operator = OPERATOR_MAP[value[0]] | _operator = OPERATOR_MAP[value[0]] | ||||
conditions = conditions.where(_operator(Field(key), value[1])) | conditions = conditions.where(_operator(Field(key), value[1])) | ||||
else: | else: | ||||
conditions = conditions.where(_operator(Field(key), value)) | |||||
if value is not None: | |||||
conditions = conditions.where(_operator(Field(key), value)) | |||||
else: | |||||
_table = conditions._from[0] | |||||
field = getattr(_table, key) | |||||
conditions = conditions.where(field.isnull()) | |||||
conditions = self.add_conditions(conditions, **kwargs) | conditions = self.add_conditions(conditions, **kwargs) | ||||
return conditions | return conditions | ||||
@@ -495,7 +495,7 @@ frappe.ui.form.on('Dashboard Chart', { | |||||
set_parent_document_type: async function(frm) { | set_parent_document_type: async function(frm) { | ||||
let document_type = frm.doc.document_type; | let document_type = frm.doc.document_type; | ||||
let doc_is_table = document_type && | |||||
let doc_is_table = document_type && | |||||
(await frappe.db.get_value('DocType', document_type, 'istable')).message.istable; | (await frappe.db.get_value('DocType', document_type, 'istable')).message.istable; | ||||
frm.set_df_property('parent_document_type', 'hidden', !doc_is_table); | frm.set_df_property('parent_document_type', 'hidden', !doc_is_table); | ||||
@@ -16,7 +16,7 @@ frappe.ui.form.on('Form Tour', { | |||||
frm.add_custom_button(__('Show Tour'), async () => { | frm.add_custom_button(__('Show Tour'), async () => { | ||||
const issingle = await check_if_single(frm.doc.reference_doctype); | const issingle = await check_if_single(frm.doc.reference_doctype); | ||||
let route_changed = null; | let route_changed = null; | ||||
if (issingle) { | if (issingle) { | ||||
route_changed = frappe.set_route('Form', frm.doc.reference_doctype); | route_changed = frappe.set_route('Form', frm.doc.reference_doctype); | ||||
} else if (frm.doc.first_document) { | } else if (frm.doc.first_document) { | ||||
@@ -76,26 +76,6 @@ def archive_restore_column(board_name, column_title, status): | |||||
return doc.columns | return doc.columns | ||||
@frappe.whitelist() | |||||
def update_doc(doc): | |||||
'''Updates the doc when card is edited''' | |||||
doc = json.loads(doc) | |||||
try: | |||||
to_update = doc | |||||
doctype = doc['doctype'] | |||||
docname = doc['name'] | |||||
doc = frappe.get_doc(doctype, docname) | |||||
doc.update(to_update) | |||||
doc.save() | |||||
except: | |||||
return { | |||||
'doc': doc, | |||||
'exc': frappe.utils.get_traceback() | |||||
} | |||||
return doc | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def update_order(board_name, order): | def update_order(board_name, order): | ||||
'''Save the order of cards in columns''' | '''Save the order of cards in columns''' | ||||
@@ -24,7 +24,7 @@ frappe.views.calendar["ToDo"] = { | |||||
"options": "reference_type", | "options": "reference_type", | ||||
"label": __("Task") | "label": __("Task") | ||||
} | } | ||||
], | ], | ||||
get_events_method: "frappe.desk.calendar.get_events" | get_events_method: "frappe.desk.calendar.get_events" | ||||
}; | }; | ||||
@@ -9,7 +9,7 @@ frappe.ui.form.on('Workspace', { | |||||
refresh: function(frm) { | refresh: function(frm) { | ||||
frm.enable_save(); | frm.enable_save(); | ||||
if (frm.doc.for_user || (frm.doc.public && !frm.has_perm('write') && | |||||
if (frm.doc.for_user || (frm.doc.public && !frm.has_perm('write') && | |||||
!frappe.user.has_role('Workspace Manager'))) { | !frappe.user.has_role('Workspace Manager'))) { | ||||
frm.trigger('disable_form'); | frm.trigger('disable_form'); | ||||
} | } | ||||
@@ -176,9 +176,9 @@ def update_page(name, title, icon, parent, public): | |||||
doc = frappe.get_doc("Workspace", name) | doc = frappe.get_doc("Workspace", name) | ||||
filters = { | |||||
filters = { | |||||
'parent_page': doc.title, | 'parent_page': doc.title, | ||||
'public': doc.public | |||||
'public': doc.public | |||||
} | } | ||||
child_docs = frappe.get_list("Workspace", filters=filters) | child_docs = frappe.get_list("Workspace", filters=filters) | ||||
@@ -255,7 +255,7 @@ def delete_page(page): | |||||
def sort_pages(sb_public_items, sb_private_items): | def sort_pages(sb_public_items, sb_private_items): | ||||
if not loads(sb_public_items) and not loads(sb_private_items): | if not loads(sb_public_items) and not loads(sb_private_items): | ||||
return | return | ||||
sb_public_items = loads(sb_public_items) | sb_public_items = loads(sb_public_items) | ||||
sb_private_items = loads(sb_private_items) | sb_private_items = loads(sb_private_items) | ||||
@@ -292,7 +292,7 @@ def last_sequence_id(doc): | |||||
if not doc_exists: | if not doc_exists: | ||||
return 0 | return 0 | ||||
return frappe.db.get_list('Workspace', | |||||
return frappe.db.get_list('Workspace', | |||||
fields=['sequence_id'], | fields=['sequence_id'], | ||||
filters={ | filters={ | ||||
'public': doc.public, | 'public': doc.public, | ||||
@@ -406,7 +406,7 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visi | |||||
for column in data.columns: | for column in data.columns: | ||||
if column.get("hidden"): | if column.get("hidden"): | ||||
continue | continue | ||||
result[0].append(column.get("label")) | |||||
result[0].append(_(column.get("label"))) | |||||
column_width = cint(column.get('width', 0)) | column_width = cint(column.get('width', 0)) | ||||
# to convert into scale accepted by openpyxl | # to convert into scale accepted by openpyxl | ||||
column_width /= 10 | column_width /= 10 | ||||
@@ -61,7 +61,7 @@ def get_context(context): | |||||
""") | """) | ||||
def validate_standard(self): | def validate_standard(self): | ||||
if self.is_standard and not frappe.conf.developer_mode: | |||||
if self.is_standard and self.enabled and not frappe.conf.developer_mode: | |||||
frappe.throw(_('Cannot edit Standard Notification. To edit, please disable this and duplicate it')) | frappe.throw(_('Cannot edit Standard Notification. To edit, please disable this and duplicate it')) | ||||
def validate_condition(self): | def validate_condition(self): | ||||
@@ -630,7 +630,7 @@ class InboundMail(Email): | |||||
if self.reference_document(): | if self.reference_document(): | ||||
data['reference_doctype'] = self.reference_document().doctype | data['reference_doctype'] = self.reference_document().doctype | ||||
data['reference_name'] = self.reference_document().name | data['reference_name'] = self.reference_document().name | ||||
else: | |||||
else: | |||||
if append_to and append_to != 'Communication': | if append_to and append_to != 'Communication': | ||||
reference_doc = self._create_reference_document(append_to) | reference_doc = self._create_reference_document(append_to) | ||||
if reference_doc: | if reference_doc: | ||||
@@ -221,7 +221,8 @@ scheduler_events = { | |||||
"frappe.deferred_insert.save_to_db", | "frappe.deferred_insert.save_to_db", | ||||
"frappe.desk.form.document_follow.send_hourly_updates", | "frappe.desk.form.document_follow.send_hourly_updates", | ||||
"frappe.integrations.doctype.google_calendar.google_calendar.sync", | "frappe.integrations.doctype.google_calendar.google_calendar.sync", | ||||
"frappe.email.doctype.newsletter.newsletter.send_scheduled_email" | |||||
"frappe.email.doctype.newsletter.newsletter.send_scheduled_email", | |||||
"frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.process_data_deletion_request" | |||||
], | ], | ||||
"daily": [ | "daily": [ | ||||
"frappe.email.queue.set_expiry_for_email_queue", | "frappe.email.queue.set_expiry_for_email_queue", | ||||
@@ -240,8 +241,7 @@ scheduler_events = { | |||||
"frappe.automation.doctype.auto_repeat.auto_repeat.set_auto_repeat_as_completed", | "frappe.automation.doctype.auto_repeat.auto_repeat.set_auto_repeat_as_completed", | ||||
"frappe.email.doctype.unhandled_email.unhandled_email.remove_old_unhandled_emails", | "frappe.email.doctype.unhandled_email.unhandled_email.remove_old_unhandled_emails", | ||||
"frappe.core.doctype.prepared_report.prepared_report.delete_expired_prepared_reports", | "frappe.core.doctype.prepared_report.prepared_report.delete_expired_prepared_reports", | ||||
"frappe.core.doctype.log_settings.log_settings.run_log_clean_up", | |||||
"frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.process_data_deletion_request" | |||||
"frappe.core.doctype.log_settings.log_settings.run_log_clean_up" | |||||
], | ], | ||||
"daily_long": [ | "daily_long": [ | ||||
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily", | "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily", | ||||
@@ -3,6 +3,6 @@ | |||||
frappe.ui.form.on('Razorpay Settings', { | frappe.ui.form.on('Razorpay Settings', { | ||||
refresh: function(frm) { | refresh: function(frm) { | ||||
} | } | ||||
}); | }); |
@@ -471,7 +471,7 @@ class Document(BaseDocument): | |||||
# We'd probably want the creation and owner to be set via API | # We'd probably want the creation and owner to be set via API | ||||
# or Data import at some point, that'd have to be handled here | # or Data import at some point, that'd have to be handled here | ||||
if self.is_new() and not (frappe.flags.in_patch or frappe.flags.in_migrate): | |||||
if self.is_new() and not (frappe.flags.in_install or frappe.flags.in_patch or frappe.flags.in_migrate): | |||||
self.creation = self.modified | self.creation = self.modified | ||||
self.owner = self.modified_by | self.owner = self.modified_by | ||||
@@ -860,14 +860,14 @@ class Document(BaseDocument): | |||||
def run_method(self, method, *args, **kwargs): | def run_method(self, method, *args, **kwargs): | ||||
"""run standard triggers, plus those in hooks""" | """run standard triggers, plus those in hooks""" | ||||
if "flags" in kwargs: | |||||
del kwargs["flags"] | |||||
if hasattr(self, method) and hasattr(getattr(self, method), "__call__"): | |||||
fn = lambda self, *args, **kwargs: getattr(self, method)(*args, **kwargs) | |||||
else: | |||||
# hack! to run hooks even if method does not exist | |||||
fn = lambda self, *args, **kwargs: None | |||||
def fn(self, *args, **kwargs): | |||||
method_object = getattr(self, method, None) | |||||
# Cannot have a field with same name as method | |||||
# If method found in __dict__, expect it to be callable | |||||
if method in self.__dict__ or callable(method_object): | |||||
return method_object(*args, **kwargs) | |||||
fn.__name__ = str(method) | fn.__name__ = str(method) | ||||
out = Document.hook(fn)(self, *args, **kwargs) | out = Document.hook(fn)(self, *args, **kwargs) | ||||
@@ -1003,8 +1003,6 @@ class Document(BaseDocument): | |||||
- `on_cancel` for **Cancel** | - `on_cancel` for **Cancel** | ||||
- `update_after_submit` for **Update after Submit**""" | - `update_after_submit` for **Update after Submit**""" | ||||
doc_before_save = self.get_doc_before_save() | |||||
if self._action=="save": | if self._action=="save": | ||||
self.run_method("on_update") | self.run_method("on_update") | ||||
elif self._action=="submit": | elif self._action=="submit": | ||||
@@ -77,13 +77,15 @@ def rename_doc( | |||||
) -> str: | ) -> str: | ||||
"""Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link".""" | """Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link".""" | ||||
if not frappe.db.exists(doctype, old): | if not frappe.db.exists(doctype, old): | ||||
frappe.errprint(_("Failed: {0} to {1} because {0} doesn't exist.").format(old, new)) | |||||
return | return | ||||
if ignore_if_exists and frappe.db.exists(doctype, new): | if ignore_if_exists and frappe.db.exists(doctype, new): | ||||
frappe.errprint(_("Failed: {0} to {1} because {1} already exists.").format(old, new)) | |||||
return | return | ||||
if old==new: | if old==new: | ||||
frappe.msgprint(_('Please select a new name to rename')) | |||||
frappe.errprint(_("Ignored: {0} to {1} no changes made because old and new name are the same.").format(old, new)) | |||||
return | return | ||||
force = cint(force) | force = cint(force) | ||||
@@ -540,15 +542,16 @@ def bulk_rename(doctype: str, rows: Optional[List[List]] = None, via_console: bo | |||||
msg = _("Successful: {0} to {1}").format(row[0], row[1]) | msg = _("Successful: {0} to {1}").format(row[0], row[1]) | ||||
frappe.db.commit() | frappe.db.commit() | ||||
else: | else: | ||||
msg = _("Ignored: {0} to {1}").format(row[0], row[1]) | |||||
msg = None | |||||
except Exception as e: | except Exception as e: | ||||
msg = _("** Failed: {0} to {1}: {2}").format(row[0], row[1], repr(e)) | msg = _("** Failed: {0} to {1}: {2}").format(row[0], row[1], repr(e)) | ||||
frappe.db.rollback() | frappe.db.rollback() | ||||
if via_console: | |||||
print(msg) | |||||
else: | |||||
rename_log.append(msg) | |||||
if msg: | |||||
if via_console: | |||||
print(msg) | |||||
else: | |||||
rename_log.append(msg) | |||||
frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', doctype=doctype) | frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', doctype=doctype) | ||||
@@ -37,6 +37,7 @@ patches by using INI like file format: | |||||
import configparser | import configparser | ||||
import time | import time | ||||
from enum import Enum | from enum import Enum | ||||
from textwrap import dedent, indent | |||||
from typing import List, Optional | from typing import List, Optional | ||||
import frappe | import frappe | ||||
@@ -148,21 +149,36 @@ def run_single(patchmodule=None, method=None, methodargs=None, force=False): | |||||
def execute_patch(patchmodule, method=None, methodargs=None): | def execute_patch(patchmodule, method=None, methodargs=None): | ||||
"""execute the patch""" | """execute the patch""" | ||||
block_user(True) | block_user(True) | ||||
frappe.db.begin() | |||||
if patchmodule.startswith("execute:"): | |||||
has_patch_file = False | |||||
patch = patchmodule.split("execute:")[1] | |||||
docstring = "" | |||||
else: | |||||
has_patch_file = True | |||||
patch = f"{patchmodule.split()[0]}.execute" | |||||
_patch = frappe.get_attr(patch) | |||||
docstring = _patch.__doc__ or "" | |||||
if docstring: | |||||
docstring = "\n" + indent(dedent(docstring), "\t") | |||||
print(f"Executing {patchmodule or methodargs} in {frappe.local.site} ({frappe.db.cur_db_name}){docstring}") | |||||
start_time = time.time() | start_time = time.time() | ||||
frappe.db.begin() | |||||
try: | try: | ||||
print('Executing {patch} in {site} ({db})'.format(patch=patchmodule or str(methodargs), | |||||
site=frappe.local.site, db=frappe.db.cur_db_name)) | |||||
if patchmodule: | if patchmodule: | ||||
if patchmodule.startswith("finally:"): | if patchmodule.startswith("finally:"): | ||||
# run run patch at the end | # run run patch at the end | ||||
frappe.flags.final_patches.append(patchmodule) | frappe.flags.final_patches.append(patchmodule) | ||||
else: | else: | ||||
if patchmodule.startswith("execute:"): | |||||
exec(patchmodule.split("execute:")[1],globals()) | |||||
if has_patch_file: | |||||
_patch() | |||||
else: | else: | ||||
frappe.get_attr(patchmodule.split()[0] + ".execute")() | |||||
exec(patch, globals()) | |||||
update_patch_log(patchmodule) | update_patch_log(patchmodule) | ||||
elif method: | elif method: | ||||
method(**methodargs) | method(**methodargs) | ||||
@@ -174,7 +190,7 @@ def execute_patch(patchmodule, method=None, methodargs=None): | |||||
frappe.db.commit() | frappe.db.commit() | ||||
end_time = time.time() | end_time = time.time() | ||||
block_user(False) | block_user(False) | ||||
print('Success: Done in {time}s'.format(time = round(end_time - start_time, 3))) | |||||
print(f"Success: Done in {round(end_time - start_time, 3)}s") | |||||
return True | return True | ||||
@@ -189,6 +189,7 @@ frappe.patches.v14_0.update_workspace2 # 20.09.2021 | |||||
frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021 | frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021 | ||||
frappe.patches.v14_0.transform_todo_schema | frappe.patches.v14_0.transform_todo_schema | ||||
frappe.patches.v14_0.remove_post_and_post_comment | frappe.patches.v14_0.remove_post_and_post_comment | ||||
frappe.patches.v14_0.reset_creation_datetime | |||||
[post_model_sync] | [post_model_sync] | ||||
frappe.patches.v14_0.drop_data_import_legacy | frappe.patches.v14_0.drop_data_import_legacy | ||||
@@ -196,3 +197,4 @@ frappe.patches.v14_0.copy_mail_data #08.03.21 | |||||
frappe.patches.v14_0.update_github_endpoints #08-11-2021 | frappe.patches.v14_0.update_github_endpoints #08-11-2021 | ||||
frappe.patches.v14_0.remove_db_aggregation | frappe.patches.v14_0.remove_db_aggregation | ||||
frappe.patches.v14_0.update_color_names_in_kanban_board_column | frappe.patches.v14_0.update_color_names_in_kanban_board_column | ||||
frappe.patches.v14_0.update_auto_account_deletion_duration |
@@ -15,7 +15,7 @@ def execute(): | |||||
for file in files: | for file in files: | ||||
file_path = file.file_url | file_path = file.file_url | ||||
file_name = file_path.split('/')[-1] | file_name = file_path.split('/')[-1] | ||||
if not file_path.startswith(('/private/', '/files/')): | if not file_path.startswith(('/private/', '/files/')): | ||||
continue | continue | ||||
@@ -0,0 +1,41 @@ | |||||
import glob | |||||
import json | |||||
import frappe | |||||
import os | |||||
from frappe.query_builder import DocType as _DocType | |||||
def execute(): | |||||
"""Resetting creation datetimes for DocTypes""" | |||||
DocType = _DocType("DocType") | |||||
doctype_jsons = glob.glob( | |||||
os.path.join("..", "apps", "frappe", "frappe", "**", "doctype", "**", "*.json") | |||||
) | |||||
frappe_modules = frappe.get_all( | |||||
"Module Def", filters={"app_name": "frappe"}, pluck="name" | |||||
) | |||||
site_doctypes = frappe.get_all( | |||||
"DocType", | |||||
filters={"module": ("in", frappe_modules), "custom": False}, | |||||
fields=["name", "creation"], | |||||
) | |||||
for dt_path in doctype_jsons: | |||||
with open(dt_path) as f: | |||||
try: | |||||
file_schema = frappe._dict(json.load(f)) | |||||
except Exception: | |||||
continue | |||||
if not file_schema.name: | |||||
continue | |||||
_site_schema = [x for x in site_doctypes if x.name == file_schema.name] | |||||
if not _site_schema: | |||||
continue | |||||
if file_schema.creation != _site_schema[0].creation: | |||||
frappe.qb.update(DocType).set( | |||||
DocType.creation, file_schema.creation | |||||
).where(DocType.name == file_schema.name).run() |
@@ -0,0 +1,5 @@ | |||||
import frappe | |||||
def execute(): | |||||
days = frappe.db.get_single_value("Website Settings", "auto_account_deletion") | |||||
frappe.db.set_value("Website Settings", None, "auto_account_deletion", days * 24) |
@@ -160,7 +160,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat | |||||
get_df_options() { | get_df_options() { | ||||
let df_options = this.df.options; | let df_options = this.df.options; | ||||
if (!df_options) return {}; | if (!df_options) return {}; | ||||
let options = {}; | let options = {}; | ||||
if (typeof df_options === 'string') { | if (typeof df_options === 'string') { | ||||
try { | try { | ||||
@@ -92,7 +92,7 @@ frappe.ui.form.ControlTable = class ControlTable extends frappe.ui.form.Control | |||||
if (frappe.model.no_value_type.includes(field.fieldtype)) { | if (frappe.model.no_value_type.includes(field.fieldtype)) { | ||||
return false; | return false; | ||||
} | } | ||||
const is_field_matching = () => { | const is_field_matching = () => { | ||||
return ( | return ( | ||||
field.fieldname.toLowerCase() === field_name || | field.fieldname.toLowerCase() === field_name || | ||||
@@ -88,6 +88,9 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for | |||||
make_quill_editor() { | make_quill_editor() { | ||||
if (this.quill) return; | if (this.quill) return; | ||||
this.quill_container = $('<div>').appendTo(this.input_area); | this.quill_container = $('<div>').appendTo(this.input_area); | ||||
if (this.df.max_height) { | |||||
$(this.quill_container).css({'max-height': this.df.max_height, 'overflow': 'auto'}); | |||||
} | |||||
this.quill = new Quill(this.quill_container[0], this.get_quill_options()); | this.quill = new Quill(this.quill_container[0], this.get_quill_options()); | ||||
this.bind_events(); | this.bind_events(); | ||||
} | } | ||||
@@ -66,7 +66,7 @@ export default class GridPagination { | |||||
} | } | ||||
// only allow numbers from 0-9 and up, down, left, right arrow keys | // only allow numbers from 0-9 and up, down, left, right arrow keys | ||||
if (charCode > 31 && (charCode < 48 || charCode > 57) && | |||||
if (charCode > 31 && (charCode < 48 || charCode > 57) && | |||||
![37, 38, 39, 40].includes(charCode)) { | ![37, 38, 39, 40].includes(charCode)) { | ||||
return false; | return false; | ||||
} | } | ||||
@@ -615,6 +615,7 @@ export default class GridRow { | |||||
if (!this.doc) { | if (!this.doc) { | ||||
$col.attr("title", txt); | $col.attr("title", txt); | ||||
} | } | ||||
df.fieldname && $col.static_area.toggleClass('reqd', Boolean(df.reqd)); | |||||
$col.df = df; | $col.df = df; | ||||
$col.column_index = ci; | $col.column_index = ci; | ||||
@@ -148,6 +148,7 @@ frappe.ui.form.save = function (frm, action, callback, btn) { | |||||
}); | }); | ||||
if (frm.is_new() && frm.meta.autoname === 'Prompt' && !frm.doc.__newname) { | if (frm.is_new() && frm.meta.autoname === 'Prompt' && !frm.doc.__newname) { | ||||
has_errors = true; | |||||
error_fields = [__('Name'), ...error_fields]; | error_fields = [__('Name'), ...error_fields]; | ||||
} | } | ||||
@@ -103,7 +103,9 @@ frappe.ui.form.Toolbar = class Toolbar { | |||||
docname, | docname, | ||||
name: new_name, | name: new_name, | ||||
title: new_title, | title: new_title, | ||||
merge | |||||
merge, | |||||
freeze: true, | |||||
freeze_message: __("Updating related fields...") | |||||
}).then(new_docname => { | }).then(new_docname => { | ||||
if (new_name != docname) { | if (new_name != docname) { | ||||
$(document).trigger("rename", [doctype, docname, new_docname || new_name]); | $(document).trigger("rename", [doctype, docname, new_docname || new_name]); | ||||
@@ -172,6 +174,7 @@ frappe.ui.form.Toolbar = class Toolbar { | |||||
d.show(); | d.show(); | ||||
d.set_primary_action(__("Rename"), (values) => { | d.set_primary_action(__("Rename"), (values) => { | ||||
d.disable_primary_action(); | d.disable_primary_action(); | ||||
d.hide(); | |||||
this.rename_document_title(values.name, values.title, values.merge) | this.rename_document_title(values.name, values.title, values.merge) | ||||
.then(() => { | .then(() => { | ||||
d.hide(); | d.hide(); | ||||
@@ -150,7 +150,7 @@ frappe.views.ListViewSelect = class ListViewSelect { | |||||
const views_wrapper = this.sidebar.sidebar.find(".views-section"); | const views_wrapper = this.sidebar.sidebar.find(".views-section"); | ||||
views_wrapper.find(".sidebar-label").html(`${__(view)}`); | views_wrapper.find(".sidebar-label").html(`${__(view)}`); | ||||
const $dropdown = views_wrapper.find(".views-dropdown"); | const $dropdown = views_wrapper.find(".views-dropdown"); | ||||
let placeholder = `${__("Select {0}", [__(view)])}`; | let placeholder = `${__("Select {0}", [__(view)])}`; | ||||
let html = ``; | let html = ``; | ||||
@@ -615,10 +615,13 @@ $.extend(frappe.model, { | |||||
}); | }); | ||||
d.set_primary_action(__("Rename"), function() { | d.set_primary_action(__("Rename"), function() { | ||||
d.hide(); | |||||
var args = d.get_values(); | var args = d.get_values(); | ||||
if(!args) return; | if(!args) return; | ||||
return frappe.call({ | return frappe.call({ | ||||
method:"frappe.rename_doc", | method:"frappe.rename_doc", | ||||
freeze: true, | |||||
freeze_message: "Updating related fields...", | |||||
args: { | args: { | ||||
doctype: doctype, | doctype: doctype, | ||||
old: docname, | old: docname, | ||||
@@ -50,6 +50,11 @@ frappe.call = function(opts) { | |||||
} | } | ||||
var args = $.extend({}, opts.args); | var args = $.extend({}, opts.args); | ||||
if (args.freeze) { | |||||
opts.freeze = opts.freeze || args.freeze; | |||||
opts.freeze_message = opts.freeze_message || args.freeze_message; | |||||
} | |||||
// cmd | // cmd | ||||
if(opts.module && opts.page) { | if(opts.module && opts.page) { | ||||
args.cmd = opts.module+'.page.'+opts.page+'.'+opts.page+'.'+opts.method; | args.cmd = opts.module+'.page.'+opts.page+'.'+opts.page+'.'+opts.method; | ||||
@@ -112,9 +112,9 @@ frappe.ui.FieldSelect = class FieldSelect { | |||||
// main table | // main table | ||||
var main_table_fields = std_filters.concat(frappe.meta.docfield_list[me.doctype]); | var main_table_fields = std_filters.concat(frappe.meta.docfield_list[me.doctype]); | ||||
$.each(frappe.utils.sort(main_table_fields, "label", "string"), function(i, df) { | $.each(frappe.utils.sort(main_table_fields, "label", "string"), function(i, df) { | ||||
let doctype = frappe.get_meta(me.doctype).istable && me.parent_doctype ? | |||||
let doctype = frappe.get_meta(me.doctype).istable && me.parent_doctype ? | |||||
me.parent_doctype : me.doctype; | me.parent_doctype : me.doctype; | ||||
// show fields where user has read access and if report hide flag is not set | // show fields where user has read access and if report hide flag is not set | ||||
if (frappe.perm.has_perm(doctype, df.permlevel, "read")) | if (frappe.perm.has_perm(doctype, df.permlevel, "read")) | ||||
me.add_field_option(df); | me.add_field_option(df); | ||||
@@ -132,9 +132,9 @@ frappe.ui.FieldSelect = class FieldSelect { | |||||
} | } | ||||
$.each(frappe.utils.sort(child_table_fields, "label", "string"), function(i, df) { | $.each(frappe.utils.sort(child_table_fields, "label", "string"), function(i, df) { | ||||
let doctype = frappe.get_meta(me.doctype).istable && me.parent_doctype ? | |||||
let doctype = frappe.get_meta(me.doctype).istable && me.parent_doctype ? | |||||
me.parent_doctype : me.doctype; | me.parent_doctype : me.doctype; | ||||
// show fields where user has read access and if report hide flag is not set | // show fields where user has read access and if report hide flag is not set | ||||
if (frappe.perm.has_perm(doctype, df.permlevel, "read")) | if (frappe.perm.has_perm(doctype, df.permlevel, "read")) | ||||
me.add_field_option(df); | me.add_field_option(df); | ||||
@@ -244,7 +244,7 @@ Object.assign(frappe.utils, { | |||||
}; | }; | ||||
return String(txt).replace( | return String(txt).replace( | ||||
/[&<>"'`=/]/g, | |||||
/[&<>"'`=/]/g, | |||||
char => escape_html_mapping[char] || char | char => escape_html_mapping[char] || char | ||||
); | ); | ||||
}, | }, | ||||
@@ -262,7 +262,7 @@ Object.assign(frappe.utils, { | |||||
}; | }; | ||||
return String(txt).replace( | return String(txt).replace( | ||||
/&|<|>|"|'|/|`|=/g, | |||||
/&|<|>|"|'|/|`|=/g, | |||||
char => unescape_html_mapping[char] || char | char => unescape_html_mapping[char] || char | ||||
); | ); | ||||
}, | }, | ||||
@@ -1435,7 +1435,7 @@ Object.assign(frappe.utils, { | |||||
// for link titles | // for link titles | ||||
frappe._link_titles = {}; | frappe._link_titles = {}; | ||||
} | } | ||||
frappe._link_titles[doctype + "::" + name] = value; | frappe._link_titles[doctype + "::" + name] = value; | ||||
}, | }, | ||||
@@ -150,18 +150,6 @@ frappe.provide("frappe.views"); | |||||
} | } | ||||
updater.set({ cards: cards }); | updater.set({ cards: cards }); | ||||
}, | }, | ||||
update_doc: function(updater, doc, card) { | |||||
var state = this; | |||||
return frappe.call({ | |||||
method: method_prefix + "update_doc", | |||||
args: { doc: doc }, | |||||
freeze: true | |||||
}).then(function(r) { | |||||
var updated_doc = r.message; | |||||
var updated_card = prepare_card(card, state, updated_doc); | |||||
fluxify.doAction('update_card', updated_card); | |||||
}); | |||||
}, | |||||
update_order_for_single_card: function(updater, card) { | update_order_for_single_card: function(updater, card) { | ||||
// cache original order | // cache original order | ||||
const _cards = this.cards.slice(); | const _cards = this.cards.slice(); | ||||
@@ -1343,7 +1343,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { | |||||
if (file_format === 'CSV') { | if (file_format === 'CSV') { | ||||
const column_row = this.columns.reduce((acc, col) => { | const column_row = this.columns.reduce((acc, col) => { | ||||
if (!col.hidden) { | if (!col.hidden) { | ||||
acc.push(col.label); | |||||
acc.push(__(col.label)); | |||||
} | } | ||||
return acc; | return acc; | ||||
}, []); | }, []); | ||||
@@ -50,7 +50,7 @@ export default class Block { | |||||
document.documentElement.addEventListener('mousemove', do_drag, false); | document.documentElement.addEventListener('mousemove', do_drag, false); | ||||
document.documentElement.addEventListener('mouseup', stop_drag, false); | document.documentElement.addEventListener('mouseup', stop_drag, false); | ||||
} | } | ||||
function do_drag(e) { | function do_drag(e) { | ||||
$(this).css("cursor", "col-resize"); | $(this).css("cursor", "col-resize"); | ||||
$('.widget').css("pointer-events", "none"); | $('.widget').css("pointer-events", "none"); | ||||
@@ -72,7 +72,7 @@ export default class Block { | |||||
} else { | } else { | ||||
window.getSelection().removeAllRanges(); | window.getSelection().removeAllRanges(); | ||||
} | } | ||||
} | |||||
} | |||||
function stop_drag() { | function stop_drag() { | ||||
$(this).css("cursor", "default"); | $(this).css("cursor", "default"); | ||||
@@ -221,7 +221,7 @@ export default class Block { | |||||
$widget_control.prepend($button); | $widget_control.prepend($button); | ||||
this.dropdown_list.forEach((item) => { | this.dropdown_list.forEach((item) => { | ||||
if ((item.label == 'Expand' || item.label == 'Shrink') && | |||||
if ((item.label == 'Expand' || item.label == 'Shrink') && | |||||
me.options && !me.options.allow_resize) { | me.options && !me.options.allow_resize) { | ||||
return; | return; | ||||
} | } | ||||
@@ -107,7 +107,7 @@ export default class Header extends Block { | |||||
if (data.text !== undefined) { | if (data.text !== undefined) { | ||||
let text = this._data.text || ''; | let text = this._data.text || ''; | ||||
const contains_html_tag = /<[a-z][\s\S]*>/i.test(text); | const contains_html_tag = /<[a-z][\s\S]*>/i.test(text); | ||||
this._element.innerHTML = contains_html_tag ? | |||||
this._element.innerHTML = contains_html_tag ? | |||||
text : `<span class="h${this._settings.default_size}">${text}</span>`; | text : `<span class="h${this._settings.default_size}">${text}</span>`; | ||||
} | } | ||||
@@ -36,7 +36,7 @@ export default class HeaderSize { | |||||
checkState(selection) { | checkState(selection) { | ||||
let termWrapper = this.api.selection.findParentTag('SPAN'); | let termWrapper = this.api.selection.findParentTag('SPAN'); | ||||
for (const h of ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) { | for (const h of ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) { | ||||
if (termWrapper && termWrapper.classList.contains(h)) { | if (termWrapper && termWrapper.classList.contains(h)) { | ||||
let num = h.match(/\d+/)[0]; | let num = h.match(/\d+/)[0]; | ||||
@@ -57,7 +57,7 @@ export default class HeaderSize { | |||||
span.innerText = range.toString(); | span.innerText = range.toString(); | ||||
this.remove_parent_tag(range, range.commonAncestorContainer, span); | this.remove_parent_tag(range, range.commonAncestorContainer, span); | ||||
range.extractContents(); | range.extractContents(); | ||||
range.insertNode(span); | range.insertNode(span); | ||||
this.api.inlineToolbar.close(); | this.api.inlineToolbar.close(); | ||||
@@ -90,7 +90,7 @@ export default class HeaderSize { | |||||
renderActions() { | renderActions() { | ||||
this.actions = document.createElement('div'); | this.actions = document.createElement('div'); | ||||
this.actions.classList = 'header-level-select'; | this.actions.classList = 'header-level-select'; | ||||
this.headerLevels = new Array(6).fill().map((_, idx) => { | this.headerLevels = new Array(6).fill().map((_, idx) => { | ||||
const $header_level = document.createElement('div'); | const $header_level = document.createElement('div'); | ||||
$header_level.classList.add(`h${idx+1}`, 'header-level'); | $header_level.classList.add(`h${idx+1}`, 'header-level'); | ||||
@@ -116,7 +116,7 @@ export default class Paragraph extends Block { | |||||
this.wrapper.appendChild(this._element); | this.wrapper.appendChild(this._element); | ||||
this._element.classList.remove('widget'); | this._element.classList.remove('widget'); | ||||
$para_control.appendTo(this.wrapper); | $para_control.appendTo(this.wrapper); | ||||
this.wrapper.classList.add('widget', 'paragraph', 'edit-mode'); | this.wrapper.classList.add('widget', 'paragraph', 'edit-mode'); | ||||
this.open_block_list(); | this.open_block_list(); | ||||
@@ -219,7 +219,7 @@ frappe.views.Workspace = class Workspace { | |||||
$sidebar[0].firstElementChild.classList.add("selected"); | $sidebar[0].firstElementChild.classList.add("selected"); | ||||
if (sidebar_page) sidebar_page.selected = true; | if (sidebar_page) sidebar_page.selected = true; | ||||
// open child sidebar section if closed | |||||
// open child sidebar section if closed | |||||
$sidebar.parent().hasClass('hidden') && | $sidebar.parent().hasClass('hidden') && | ||||
$sidebar.parent().removeClass('hidden'); | $sidebar.parent().removeClass('hidden'); | ||||
@@ -244,7 +244,7 @@ frappe.views.Workspace = class Workspace { | |||||
this.pages[page.name] = data.message; | this.pages[page.name] = data.message; | ||||
if (!this.page_data || Object.keys(this.page_data).length === 0) return; | if (!this.page_data || Object.keys(this.page_data).length === 0) return; | ||||
if (this.page_data.charts && this.page_data.charts.items.length === 0) return; | |||||
if (this.page_data.charts && this.page_data.charts.items.length === 0) return; | |||||
return frappe.dashboard_utils.get_dashboard_settings().then(settings => { | return frappe.dashboard_utils.get_dashboard_settings().then(settings => { | ||||
if (settings) { | if (settings) { | ||||
@@ -596,9 +596,9 @@ frappe.views.Workspace = class Workspace { | |||||
} | } | ||||
update_cached_values(old_item, new_item, duplicate, new_page) { | update_cached_values(old_item, new_item, duplicate, new_page) { | ||||
let [from_pages, to_pages] = old_item.public ? | |||||
let [from_pages, to_pages] = old_item.public ? | |||||
[this.public_pages, this.private_pages] : [this.private_pages, this.public_pages]; | [this.public_pages, this.private_pages] : [this.private_pages, this.public_pages]; | ||||
let old_item_index = from_pages.findIndex(page => page.title == old_item.title); | let old_item_index = from_pages.findIndex(page => page.title == old_item.title); | ||||
duplicate && old_item_index++; | duplicate && old_item_index++; | ||||
@@ -859,7 +859,7 @@ frappe.views.Workspace = class Workspace { | |||||
public: page.attributes['item-public'].value | public: page.attributes['item-public'].value | ||||
}); | }); | ||||
let $drop_icon = $(page).find('.sidebar-item-control .drop-icon').first(); | |||||
let $drop_icon = $(page).find('.sidebar-item-control .drop-icon').first(); | |||||
if ($(page).find('.sidebar-child-item > *').length != 0) { | if ($(page).find('.sidebar-child-item > *').length != 0) { | ||||
$drop_icon.removeClass('hidden'); | $drop_icon.removeClass('hidden'); | ||||
} else { | } else { | ||||
@@ -993,13 +993,13 @@ frappe.views.Workspace = class Workspace { | |||||
} | } | ||||
} | } | ||||
}); | }); | ||||
this.update_cached_values(new_page, new_page, true, true); | this.update_cached_values(new_page, new_page, true, true); | ||||
let pre_url = new_page.public ? '' : 'private/'; | let pre_url = new_page.public ? '' : 'private/'; | ||||
let route = pre_url + frappe.router.slug(new_page.title); | let route = pre_url + frappe.router.slug(new_page.title); | ||||
frappe.set_route(route); | frappe.set_route(route); | ||||
this.make_sidebar(); | this.make_sidebar(); | ||||
this.show_sidebar_actions(); | this.show_sidebar_actions(); | ||||
}); | }); | ||||
@@ -1010,15 +1010,15 @@ frappe.views.Workspace = class Workspace { | |||||
validate_page(new_page, old_page) { | validate_page(new_page, old_page) { | ||||
let message = ""; | let message = ""; | ||||
let [from_pages, to_pages] = new_page.is_public ? | |||||
let [from_pages, to_pages] = new_page.is_public ? | |||||
[this.private_pages, this.public_pages] : [this.public_pages, this.private_pages]; | [this.private_pages, this.public_pages] : [this.public_pages, this.private_pages]; | ||||
let section = this.sidebar_categories[new_page.is_public]; | let section = this.sidebar_categories[new_page.is_public]; | ||||
if (to_pages && to_pages.filter(p => p.title == new_page.title)[0]) { | if (to_pages && to_pages.filter(p => p.title == new_page.title)[0]) { | ||||
message = `Page with title ${new_page.title} already exist.`; | message = `Page with title ${new_page.title} already exist.`; | ||||
} | |||||
} | |||||
if (frappe.router.doctype_route_exist(frappe.router.slug(new_page.title))) { | if (frappe.router.doctype_route_exist(frappe.router.slug(new_page.title))) { | ||||
message = "Doctype with same route already exist. Please choose different title."; | message = "Doctype with same route already exist. Please choose different title."; | ||||
} | } | ||||
@@ -698,12 +698,12 @@ export default class ChartWidget extends Widget { | |||||
.get_filters_for_chart_type(this.chart_doc).then(filters => { | .get_filters_for_chart_type(this.chart_doc).then(filters => { | ||||
chart_saved_filters = this.update_default_date_filters(filters, chart_saved_filters); | chart_saved_filters = this.update_default_date_filters(filters, chart_saved_filters); | ||||
this.filters = | this.filters = | ||||
frappe.utils.parse_array(user_saved_filters) || frappe.utils.parse_array(this.filters) | |||||
frappe.utils.parse_array(user_saved_filters) || frappe.utils.parse_array(this.filters) | |||||
|| frappe.utils.parse_array(chart_saved_filters); | || frappe.utils.parse_array(chart_saved_filters); | ||||
}); | }); | ||||
} else { | } else { | ||||
this.filters = | this.filters = | ||||
frappe.utils.parse_array(user_saved_filters) || frappe.utils.parse_array(this.filters) | |||||
frappe.utils.parse_array(user_saved_filters) || frappe.utils.parse_array(this.filters) | |||||
|| frappe.utils.parse_array(chart_saved_filters); | || frappe.utils.parse_array(chart_saved_filters); | ||||
return Promise.resolve(); | return Promise.resolve(); | ||||
} | } | ||||
@@ -993,7 +993,7 @@ jSignatureClass.prototype.resetCanvas = function(data, dontClear){ | |||||
ctx.shadowBlur = 0; | ctx.shadowBlur = 0; | ||||
} | } | ||||
} | } | ||||
ctx.strokeStyle = settings.color; | ctx.strokeStyle = settings.color; | ||||
// setting up new dataEngine | // setting up new dataEngine | ||||
@@ -12,7 +12,7 @@ | |||||
*/ | */ | ||||
/* | /* | ||||
1. Buttons | 1. Buttons | ||||
*/ | */ | ||||
@@ -257,7 +257,7 @@ a.pswp__share--download:hover { | |||||
padding: 0 10px; } | padding: 0 10px; } | ||||
/* | /* | ||||
4. Caption | 4. Caption | ||||
*/ | */ | ||||
@@ -338,8 +338,8 @@ a.pswp__share--download:hover { | |||||
margin: 0; } | margin: 0; } | ||||
.pswp--css_animation .pswp__preloader__cut { | .pswp--css_animation .pswp__preloader__cut { | ||||
/* | |||||
The idea of animating inner circle is based on Polymer ("material") loading indicator | |||||
/* | |||||
The idea of animating inner circle is based on Polymer ("material") loading indicator | |||||
by Keanu Lee https://blog.keanulee.com/2014/10/20/the-tale-of-three-spinners.html | by Keanu Lee https://blog.keanulee.com/2014/10/20/the-tale-of-three-spinners.html | ||||
*/ | */ | ||||
position: relative; | position: relative; | ||||
@@ -409,7 +409,7 @@ a.pswp__share--download:hover { | |||||
transform: rotate(0); } } | transform: rotate(0); } } | ||||
/* | /* | ||||
6. Additional styles | 6. Additional styles | ||||
*/ | */ | ||||
@@ -5,9 +5,9 @@ | |||||
* | * | ||||
* UI on top of main sliding area (caption, arrows, close button, etc.). | * UI on top of main sliding area (caption, arrows, close button, etc.). | ||||
* Built just using public methods/properties of PhotoSwipe. | * Built just using public methods/properties of PhotoSwipe. | ||||
* | |||||
* | |||||
*/ | */ | ||||
(function (root, factory) { | |||||
(function (root, factory) { | |||||
if (typeof define === 'function' && define.amd) { | if (typeof define === 'function' && define.amd) { | ||||
define(factory); | define(factory); | ||||
} else if (typeof exports === 'object') { | } else if (typeof exports === 'object') { | ||||
@@ -48,11 +48,11 @@ var PhotoSwipeUI_Default = | |||||
_options, | _options, | ||||
_defaultUIOptions = { | _defaultUIOptions = { | ||||
barsSize: {top:44, bottom:'auto'}, | barsSize: {top:44, bottom:'auto'}, | ||||
closeElClasses: ['item', 'caption', 'zoom-wrap', 'ui', 'top-bar'], | |||||
timeToIdle: 4000, | |||||
closeElClasses: ['item', 'caption', 'zoom-wrap', 'ui', 'top-bar'], | |||||
timeToIdle: 4000, | |||||
timeToIdleOutside: 1000, | timeToIdleOutside: 1000, | ||||
loadingIndicatorDelay: 1000, // 2s | loadingIndicatorDelay: 1000, // 2s | ||||
addCaptionHTMLFn: function(item, captionEl /*, isFake */) { | addCaptionHTMLFn: function(item, captionEl /*, isFake */) { | ||||
if(!item.title) { | if(!item.title) { | ||||
captionEl.children[0].innerHTML = ''; | captionEl.children[0].innerHTML = ''; | ||||
@@ -92,7 +92,7 @@ var PhotoSwipeUI_Default = | |||||
getTextForShare: function( /* shareButtonData */ ) { | getTextForShare: function( /* shareButtonData */ ) { | ||||
return pswp.currItem.title || ''; | return pswp.currItem.title || ''; | ||||
}, | }, | ||||
indexIndicatorSep: ' / ', | indexIndicatorSep: ' / ', | ||||
fitControlsWidth: 1200 | fitControlsWidth: 1200 | ||||
@@ -136,12 +136,12 @@ var PhotoSwipeUI_Default = | |||||
} | } | ||||
_blockControlsTap = true; | _blockControlsTap = true; | ||||
// Some versions of Android don't prevent ghost click event | |||||
// Some versions of Android don't prevent ghost click event | |||||
// when preventDefault() was called on touchstart and/or touchend. | // when preventDefault() was called on touchstart and/or touchend. | ||||
// | |||||
// This happens on v4.3, 4.2, 4.1, | |||||
// older versions strangely work correctly, | |||||
// but just in case we add delay on all of them) | |||||
// | |||||
// This happens on v4.3, 4.2, 4.1, | |||||
// older versions strangely work correctly, | |||||
// but just in case we add delay on all of them) | |||||
var tapDelay = framework.features.isOldAndroid ? 600 : 30; | var tapDelay = framework.features.isOldAndroid ? 600 : 30; | ||||
_blockControlsTapTimeout = setTimeout(function() { | _blockControlsTapTimeout = setTimeout(function() { | ||||
_blockControlsTap = false; | _blockControlsTap = false; | ||||
@@ -172,8 +172,8 @@ var PhotoSwipeUI_Default = | |||||
_toggleShareModal = function() { | _toggleShareModal = function() { | ||||
_shareModalHidden = !_shareModalHidden; | _shareModalHidden = !_shareModalHidden; | ||||
if(!_shareModalHidden) { | if(!_shareModalHidden) { | ||||
_toggleShareModalClass(); | _toggleShareModalClass(); | ||||
setTimeout(function() { | setTimeout(function() { | ||||
@@ -189,7 +189,7 @@ var PhotoSwipeUI_Default = | |||||
} | } | ||||
}, 300); | }, 300); | ||||
} | } | ||||
if(!_shareModalHidden) { | if(!_shareModalHidden) { | ||||
_updateShareURLs(); | _updateShareURLs(); | ||||
} | } | ||||
@@ -211,13 +211,13 @@ var PhotoSwipeUI_Default = | |||||
} | } | ||||
window.open(target.href, 'pswp_share', 'scrollbars=yes,resizable=yes,toolbar=no,'+ | window.open(target.href, 'pswp_share', 'scrollbars=yes,resizable=yes,toolbar=no,'+ | ||||
'location=yes,width=550,height=420,top=100,left=' + | |||||
'location=yes,width=550,height=420,top=100,left=' + | |||||
(window.screen ? Math.round(screen.width / 2 - 275) : 100) ); | (window.screen ? Math.round(screen.width / 2 - 275) : 100) ); | ||||
if(!_shareModalHidden) { | if(!_shareModalHidden) { | ||||
_toggleShareModal(); | _toggleShareModal(); | ||||
} | } | ||||
return false; | return false; | ||||
}, | }, | ||||
_updateShareURLs = function() { | _updateShareURLs = function() { | ||||
@@ -242,7 +242,7 @@ var PhotoSwipeUI_Default = | |||||
shareButtonOut += '<a href="' + shareURL + '" target="_blank" '+ | shareButtonOut += '<a href="' + shareURL + '" target="_blank" '+ | ||||
'class="pswp__share--' + shareButtonData.id + '"' + | 'class="pswp__share--' + shareButtonData.id + '"' + | ||||
(shareButtonData.download ? 'download' : '') + '>' + | |||||
(shareButtonData.download ? 'download' : '') + '>' + | |||||
shareButtonData.label + '</a>'; | shareButtonData.label + '</a>'; | ||||
if(_options.parseShareButtonOut) { | if(_options.parseShareButtonOut) { | ||||
@@ -297,7 +297,7 @@ var PhotoSwipeUI_Default = | |||||
_setupLoadingIndicator = function() { | _setupLoadingIndicator = function() { | ||||
// Setup loading indicator | // Setup loading indicator | ||||
if(_options.preloaderEl) { | if(_options.preloaderEl) { | ||||
_toggleLoadingIndicator(true); | _toggleLoadingIndicator(true); | ||||
_listen('beforeChange', function() { | _listen('beforeChange', function() { | ||||
@@ -310,18 +310,18 @@ var PhotoSwipeUI_Default = | |||||
if(pswp.currItem && pswp.currItem.loading) { | if(pswp.currItem && pswp.currItem.loading) { | ||||
if( !pswp.allowProgressiveImg() || (pswp.currItem.img && !pswp.currItem.img.naturalWidth) ) { | if( !pswp.allowProgressiveImg() || (pswp.currItem.img && !pswp.currItem.img.naturalWidth) ) { | ||||
// show preloader if progressive loading is not enabled, | |||||
// show preloader if progressive loading is not enabled, | |||||
// or image width is not defined yet (because of slow connection) | // or image width is not defined yet (because of slow connection) | ||||
_toggleLoadingIndicator(false); | |||||
_toggleLoadingIndicator(false); | |||||
// items-controller.js function allowProgressiveImg | // items-controller.js function allowProgressiveImg | ||||
} | } | ||||
} else { | } else { | ||||
_toggleLoadingIndicator(true); // hide preloader | _toggleLoadingIndicator(true); // hide preloader | ||||
} | } | ||||
}, _options.loadingIndicatorDelay); | }, _options.loadingIndicatorDelay); | ||||
}); | }); | ||||
_listen('imageLoadComplete', function(index, item) { | _listen('imageLoadComplete', function(index, item) { | ||||
if(pswp.currItem === item) { | if(pswp.currItem === item) { | ||||
@@ -341,8 +341,8 @@ var PhotoSwipeUI_Default = | |||||
var gap = item.vGap; | var gap = item.vGap; | ||||
if( _fitControlsInViewport() ) { | if( _fitControlsInViewport() ) { | ||||
var bars = _options.barsSize; | |||||
var bars = _options.barsSize; | |||||
if(_options.captionEl && bars.bottom === 'auto') { | if(_options.captionEl && bars.bottom === 'auto') { | ||||
if(!_fakeCaptionContainer) { | if(!_fakeCaptionContainer) { | ||||
_fakeCaptionContainer = framework.createEl('pswp__caption pswp__caption--fake'); | _fakeCaptionContainer = framework.createEl('pswp__caption pswp__caption--fake'); | ||||
@@ -360,7 +360,7 @@ var PhotoSwipeUI_Default = | |||||
} else { | } else { | ||||
gap.bottom = bars.bottom === 'auto' ? 0 : bars.bottom; | gap.bottom = bars.bottom === 'auto' ? 0 : bars.bottom; | ||||
} | } | ||||
// height of top bar is static, no need to calculate it | // height of top bar is static, no need to calculate it | ||||
gap.top = bars.top; | gap.top = bars.top; | ||||
} else { | } else { | ||||
@@ -371,7 +371,7 @@ var PhotoSwipeUI_Default = | |||||
// Hide controls when mouse is used | // Hide controls when mouse is used | ||||
if(_options.timeToIdle) { | if(_options.timeToIdle) { | ||||
_listen('mouseUsed', function() { | _listen('mouseUsed', function() { | ||||
framework.bind(document, 'mousemove', _onIdleMouseMove); | framework.bind(document, 'mousemove', _onIdleMouseMove); | ||||
framework.bind(document, 'mouseout', _onMouseLeaveWindow); | framework.bind(document, 'mouseout', _onMouseLeaveWindow); | ||||
@@ -418,77 +418,77 @@ var PhotoSwipeUI_Default = | |||||
var _uiElements = [ | var _uiElements = [ | ||||
{ | |||||
name: 'caption', | |||||
{ | |||||
name: 'caption', | |||||
option: 'captionEl', | option: 'captionEl', | ||||
onInit: function(el) { | |||||
_captionContainer = el; | |||||
} | |||||
onInit: function(el) { | |||||
_captionContainer = el; | |||||
} | |||||
}, | }, | ||||
{ | |||||
name: 'share-modal', | |||||
{ | |||||
name: 'share-modal', | |||||
option: 'shareEl', | option: 'shareEl', | ||||
onInit: function(el) { | |||||
onInit: function(el) { | |||||
_shareModal = el; | _shareModal = el; | ||||
}, | }, | ||||
onTap: function() { | onTap: function() { | ||||
_toggleShareModal(); | _toggleShareModal(); | ||||
} | |||||
} | |||||
}, | }, | ||||
{ | |||||
name: 'button--share', | |||||
{ | |||||
name: 'button--share', | |||||
option: 'shareEl', | option: 'shareEl', | ||||
onInit: function(el) { | |||||
onInit: function(el) { | |||||
_shareButton = el; | _shareButton = el; | ||||
}, | }, | ||||
onTap: function() { | onTap: function() { | ||||
_toggleShareModal(); | _toggleShareModal(); | ||||
} | |||||
} | |||||
}, | }, | ||||
{ | |||||
name: 'button--zoom', | |||||
{ | |||||
name: 'button--zoom', | |||||
option: 'zoomEl', | option: 'zoomEl', | ||||
onTap: pswp.toggleDesktopZoom | onTap: pswp.toggleDesktopZoom | ||||
}, | }, | ||||
{ | |||||
name: 'counter', | |||||
{ | |||||
name: 'counter', | |||||
option: 'counterEl', | option: 'counterEl', | ||||
onInit: function(el) { | |||||
onInit: function(el) { | |||||
_indexIndicator = el; | _indexIndicator = el; | ||||
} | |||||
} | |||||
}, | }, | ||||
{ | |||||
name: 'button--close', | |||||
{ | |||||
name: 'button--close', | |||||
option: 'closeEl', | option: 'closeEl', | ||||
onTap: pswp.close | onTap: pswp.close | ||||
}, | }, | ||||
{ | |||||
name: 'button--arrow--left', | |||||
{ | |||||
name: 'button--arrow--left', | |||||
option: 'arrowEl', | option: 'arrowEl', | ||||
onTap: pswp.prev | onTap: pswp.prev | ||||
}, | }, | ||||
{ | |||||
name: 'button--arrow--right', | |||||
{ | |||||
name: 'button--arrow--right', | |||||
option: 'arrowEl', | option: 'arrowEl', | ||||
onTap: pswp.next | onTap: pswp.next | ||||
}, | }, | ||||
{ | |||||
name: 'button--fs', | |||||
{ | |||||
name: 'button--fs', | |||||
option: 'fullscreenEl', | option: 'fullscreenEl', | ||||
onTap: function() { | |||||
onTap: function() { | |||||
if(_fullscrenAPI.isFullscreen()) { | if(_fullscrenAPI.isFullscreen()) { | ||||
_fullscrenAPI.exit(); | _fullscrenAPI.exit(); | ||||
} else { | } else { | ||||
_fullscrenAPI.enter(); | _fullscrenAPI.enter(); | ||||
} | } | ||||
} | |||||
} | |||||
}, | }, | ||||
{ | |||||
name: 'preloader', | |||||
{ | |||||
name: 'preloader', | |||||
option: 'preloaderEl', | option: 'preloaderEl', | ||||
onInit: function(el) { | |||||
onInit: function(el) { | |||||
_loadingIndicator = el; | _loadingIndicator = el; | ||||
} | |||||
} | |||||
} | } | ||||
]; | ]; | ||||
@@ -514,12 +514,12 @@ var PhotoSwipeUI_Default = | |||||
if(classAttr.indexOf('pswp__' + uiElement.name) > -1 ) { | if(classAttr.indexOf('pswp__' + uiElement.name) > -1 ) { | ||||
if( _options[uiElement.option] ) { // if element is not disabled from options | if( _options[uiElement.option] ) { // if element is not disabled from options | ||||
framework.removeClass(item, 'pswp__element--disabled'); | framework.removeClass(item, 'pswp__element--disabled'); | ||||
if(uiElement.onInit) { | if(uiElement.onInit) { | ||||
uiElement.onInit(item); | uiElement.onInit(item); | ||||
} | } | ||||
//item.style.display = 'block'; | //item.style.display = 'block'; | ||||
} else { | } else { | ||||
framework.addClass(item, 'pswp__element--disabled'); | framework.addClass(item, 'pswp__element--disabled'); | ||||
@@ -538,7 +538,7 @@ var PhotoSwipeUI_Default = | |||||
}; | }; | ||||
ui.init = function() { | ui.init = function() { | ||||
@@ -574,9 +574,9 @@ var PhotoSwipeUI_Default = | |||||
_listen('preventDragEvent', function(e, isDown, preventObj) { | _listen('preventDragEvent', function(e, isDown, preventObj) { | ||||
var t = e.target || e.srcElement; | var t = e.target || e.srcElement; | ||||
if( | if( | ||||
t && | |||||
t.getAttribute('class') && e.type.indexOf('mouse') > -1 && | |||||
( t.getAttribute('class').indexOf('__caption') > 0 || (/(SMALL|STRONG|EM)/i).test(t.tagName) ) | |||||
t && | |||||
t.getAttribute('class') && e.type.indexOf('mouse') > -1 && | |||||
( t.getAttribute('class').indexOf('__caption') > 0 || (/(SMALL|STRONG|EM)/i).test(t.tagName) ) | |||||
) { | ) { | ||||
preventObj.prevent = false; | preventObj.prevent = false; | ||||
} | } | ||||
@@ -634,7 +634,7 @@ var PhotoSwipeUI_Default = | |||||
framework.addClass( _controls, 'pswp__ui--hidden'); | framework.addClass( _controls, 'pswp__ui--hidden'); | ||||
ui.setIdle(false); | ui.setIdle(false); | ||||
}); | }); | ||||
if(!_options.showAnimationDuration) { | if(!_options.showAnimationDuration) { | ||||
framework.removeClass( _controls, 'pswp__ui--hidden'); | framework.removeClass( _controls, 'pswp__ui--hidden'); | ||||
@@ -649,7 +649,7 @@ var PhotoSwipeUI_Default = | |||||
}); | }); | ||||
_listen('parseVerticalMargin', _applyNavBarGaps); | _listen('parseVerticalMargin', _applyNavBarGaps); | ||||
_setupUIElements(); | _setupUIElements(); | ||||
if(_options.shareEl && _shareButton && _shareModal) { | if(_options.shareEl && _shareButton && _shareModal) { | ||||
@@ -673,7 +673,7 @@ var PhotoSwipeUI_Default = | |||||
ui.update = function() { | ui.update = function() { | ||||
// Don't update UI if it's hidden | // Don't update UI if it's hidden | ||||
if(_controlsVisible && pswp.currItem) { | if(_controlsVisible && pswp.currItem) { | ||||
ui.updateIndexIndicator(); | ui.updateIndexIndicator(); | ||||
if(_options.captionEl) { | if(_options.captionEl) { | ||||
@@ -704,19 +704,19 @@ var PhotoSwipeUI_Default = | |||||
pswp.setScrollOffset( 0, framework.getScrollY() ); | pswp.setScrollOffset( 0, framework.getScrollY() ); | ||||
}, 50); | }, 50); | ||||
} | } | ||||
// toogle pswp--fs class on root element | // toogle pswp--fs class on root element | ||||
framework[ (_fullscrenAPI.isFullscreen() ? 'add' : 'remove') + 'Class' ](pswp.template, 'pswp--fs'); | framework[ (_fullscrenAPI.isFullscreen() ? 'add' : 'remove') + 'Class' ](pswp.template, 'pswp--fs'); | ||||
}; | }; | ||||
ui.updateIndexIndicator = function() { | ui.updateIndexIndicator = function() { | ||||
if(_options.counterEl) { | if(_options.counterEl) { | ||||
_indexIndicator.innerHTML = (pswp.getCurrentIndex()+1) + | |||||
_options.indexIndicatorSep + | |||||
_indexIndicator.innerHTML = (pswp.getCurrentIndex()+1) + | |||||
_options.indexIndicatorSep + | |||||
_options.getNumItemsFn(); | _options.getNumItemsFn(); | ||||
} | } | ||||
}; | }; | ||||
ui.onGlobalTap = function(e) { | ui.onGlobalTap = function(e) { | ||||
e = e || window.event; | e = e || window.event; | ||||
var target = e.target || e.srcElement; | var target = e.target || e.srcElement; | ||||
@@ -742,7 +742,7 @@ var PhotoSwipeUI_Default = | |||||
pswp.toggleDesktopZoom(e.detail.releasePoint); | pswp.toggleDesktopZoom(e.detail.releasePoint); | ||||
} | } | ||||
} | } | ||||
} else { | } else { | ||||
// tap anywhere (except buttons) to toggle visibility of controls | // tap anywhere (except buttons) to toggle visibility of controls | ||||
@@ -759,7 +759,7 @@ var PhotoSwipeUI_Default = | |||||
pswp.close(); | pswp.close(); | ||||
return; | return; | ||||
} | } | ||||
} | } | ||||
}; | }; | ||||
ui.onMouseOver = function(e) { | ui.onMouseOver = function(e) { | ||||
@@ -809,7 +809,7 @@ var PhotoSwipeUI_Default = | |||||
eventK: 'moz' + tF | eventK: 'moz' + tF | ||||
}; | }; | ||||
} else if(dE.webkitRequestFullscreen) { | } else if(dE.webkitRequestFullscreen) { | ||||
api = { | api = { | ||||
@@ -829,21 +829,21 @@ var PhotoSwipeUI_Default = | |||||
} | } | ||||
if(api) { | if(api) { | ||||
api.enter = function() { | |||||
api.enter = function() { | |||||
// disable close-on-scroll in fullscreen | // disable close-on-scroll in fullscreen | ||||
_initalCloseOnScrollValue = _options.closeOnScroll; | |||||
_options.closeOnScroll = false; | |||||
_initalCloseOnScrollValue = _options.closeOnScroll; | |||||
_options.closeOnScroll = false; | |||||
if(this.enterK === 'webkitRequestFullscreen') { | if(this.enterK === 'webkitRequestFullscreen') { | ||||
pswp.template[this.enterK]( Element.ALLOW_KEYBOARD_INPUT ); | pswp.template[this.enterK]( Element.ALLOW_KEYBOARD_INPUT ); | ||||
} else { | } else { | ||||
return pswp.template[this.enterK](); | |||||
return pswp.template[this.enterK](); | |||||
} | } | ||||
}; | }; | ||||
api.exit = function() { | |||||
api.exit = function() { | |||||
_options.closeOnScroll = _initalCloseOnScrollValue; | _options.closeOnScroll = _initalCloseOnScrollValue; | ||||
return document[this.exitK](); | |||||
return document[this.exitK](); | |||||
}; | }; | ||||
api.isFullscreen = function() { return document[this.elementK]; }; | api.isFullscreen = function() { return document[this.elementK]; }; | ||||
@@ -11,10 +11,10 @@ function prettyDate(time){ | |||||
var date = new Date((time || "").replace(/-/g,"/").replace(/[TZ]/g," ").replace(/\.[0-9]*/, "")), | var date = new Date((time || "").replace(/-/g,"/").replace(/[TZ]/g," ").replace(/\.[0-9]*/, "")), | ||||
diff = (((new Date()).getTime() - date.getTime()) / 1000), | diff = (((new Date()).getTime() - date.getTime()) / 1000), | ||||
day_diff = Math.floor(diff / 86400); | day_diff = Math.floor(diff / 86400); | ||||
if ( isNaN(day_diff) || day_diff < 0 ) | if ( isNaN(day_diff) || day_diff < 0 ) | ||||
return ''; | return ''; | ||||
return day_diff == 0 && ( | return day_diff == 0 && ( | ||||
diff < 60 && "just now" || | diff < 60 && "just now" || | ||||
diff < 120 && "1 minute ago" || | diff < 120 && "1 minute ago" || | ||||
@@ -12,6 +12,13 @@ | |||||
border-bottom: 1px solid var(--table-border-color); | border-bottom: 1px solid var(--table-border-color); | ||||
color: var(--text-muted); | color: var(--text-muted); | ||||
font-size: var(--text-md); | font-size: var(--text-md); | ||||
.grid-static-col { | |||||
.static-area.reqd:after { | |||||
content: ' *'; | |||||
color: var(--red-400); | |||||
} | |||||
} | |||||
} | } | ||||
.rows .grid-row .data-row, | .rows .grid-row .data-row, | ||||
@@ -1070,11 +1070,11 @@ body { | |||||
} | } | ||||
.resizer { | .resizer { | ||||
width: 10px; | |||||
width: 10px; | |||||
height: 100%; | height: 100%; | ||||
position:absolute; | |||||
right: 0; | |||||
bottom: 0; | |||||
position:absolute; | |||||
right: 0; | |||||
bottom: 0; | |||||
cursor: col-resize; | cursor: col-resize; | ||||
border-color: transparent; | border-color: transparent; | ||||
transition: border-color 0.3s ease-in-out; | transition: border-color 0.3s ease-in-out; | ||||
@@ -1089,8 +1089,8 @@ body { | |||||
margin-bottom: 0 !important; | margin-bottom: 0 !important; | ||||
flex: 1; | flex: 1; | ||||
&:focus { | |||||
outline: none; | |||||
&:focus { | |||||
outline: none; | |||||
} | } | ||||
} | } | ||||
@@ -1124,11 +1124,11 @@ body { | |||||
color: var(--text-muted); | color: var(--text-muted); | ||||
border: 1px dashed var(--gray-400); | border: 1px dashed var(--gray-400); | ||||
cursor: pointer; | cursor: pointer; | ||||
.widget-control > * { | .widget-control > * { | ||||
width: auto; | width: auto; | ||||
} | } | ||||
.spacer-left { | .spacer-left { | ||||
min-width: 74px; | min-width: 74px; | ||||
} | } | ||||
@@ -1158,7 +1158,7 @@ body { | |||||
gap: 5px; | gap: 5px; | ||||
background-color: var(--card-bg); | background-color: var(--card-bg); | ||||
padding-left: 5px; | padding-left: 5px; | ||||
.drag-handle { | .drag-handle { | ||||
cursor: all-scroll; | cursor: all-scroll; | ||||
cursor: grabbing; | cursor: grabbing; | ||||
@@ -1325,7 +1325,7 @@ body { | |||||
padding: 6px 10px; | padding: 6px 10px; | ||||
font-size: small; | font-size: small; | ||||
border-radius: var(--border-radius-sm); | border-radius: var(--border-radius-sm); | ||||
margin: 1px 0px; | |||||
margin: 1px 0px; | |||||
} | } | ||||
.dropdown-item-icon { | .dropdown-item-icon { | ||||
@@ -202,7 +202,7 @@ $level-margin-right: 8px; | |||||
box-shadow: none; | box-shadow: none; | ||||
margin-left: 0px !important; | margin-left: 0px !important; | ||||
border: 1px solid var(--dark-border-color); | border: 1px solid var(--dark-border-color); | ||||
&.btn-info { | &.btn-info { | ||||
background-color: var(--gray-400); | background-color: var(--gray-400); | ||||
border-color: var(--gray-400); | border-color: var(--gray-400); | ||||
@@ -150,7 +150,7 @@ body { | |||||
min-width: 50%; | min-width: 50%; | ||||
padding: 0 4px; | padding: 0 4px; | ||||
margin-bottom: var(--margin-md); | margin-bottom: var(--margin-md); | ||||
&:last-child { | &:last-child { | ||||
margin-bottom: 0; | margin-bottom: 0; | ||||
} | } | ||||
@@ -163,18 +163,18 @@ | |||||
padding: var(--padding-lg); | padding: var(--padding-lg); | ||||
box-shadow: var(--card-shadow); | box-shadow: var(--card-shadow); | ||||
border-radius: var(--border-radius-md); | border-radius: var(--border-radius-md); | ||||
.new-comment-fields { | .new-comment-fields { | ||||
flex: 1; | flex: 1; | ||||
.form-label { | .form-label { | ||||
font-weight: var(--text-bold); | font-weight: var(--text-bold); | ||||
} | } | ||||
.comment-text-area textarea { | .comment-text-area textarea { | ||||
resize: none; | resize: none; | ||||
} | } | ||||
@media (min-width: 576px) { | @media (min-width: 576px) { | ||||
.comment-by { | .comment-by { | ||||
padding-right: 0px !important; | padding-right: 0px !important; | ||||
@@ -184,7 +184,7 @@ | |||||
} | } | ||||
} | } | ||||
} | } | ||||
#comment-list { | #comment-list { | ||||
position: relative; | position: relative; | ||||
@@ -206,7 +206,7 @@ | |||||
top: 10px; | top: 10px; | ||||
left: -17px; | left: -17px; | ||||
} | } | ||||
.comment-content { | .comment-content { | ||||
box-shadow: var(--card-shadow); | box-shadow: var(--card-shadow); | ||||
border-radius: var(--border-radius-md); | border-radius: var(--border-radius-md); | ||||
@@ -9,7 +9,7 @@ | |||||
width: 80% | width: 80% | ||||
} | } | ||||
} | } | ||||
.back-to-home { | .back-to-home { | ||||
font-size: var(--text-base); | font-size: var(--text-base); | ||||
} | } | ||||
@@ -80,6 +80,8 @@ | |||||
.dropdown-menu { | .dropdown-menu { | ||||
padding: 0.25rem; | padding: 0.25rem; | ||||
box-shadow: var(--shadow-lg); | |||||
border-color: var(--gray-200); | |||||
} | } | ||||
.dropdown-item { | .dropdown-item { | ||||
@@ -308,4 +310,4 @@ h5.modal-title { | |||||
.empty-list-icon { | .empty-list-icon { | ||||
height: 70px; | height: 70px; | ||||
} | |||||
} |
@@ -46,7 +46,7 @@ | |||||
.navbar-toggler { | .navbar-toggler { | ||||
border-color: rgba(255,255,255, 0.1); | border-color: rgba(255,255,255, 0.1); | ||||
.icon { | .icon { | ||||
stroke: none; | stroke: none; | ||||
} | } | ||||
@@ -1,6 +1,6 @@ | |||||
.portal-row { | .portal-row { | ||||
padding: 1rem 0; | padding: 1rem 0; | ||||
a { | a { | ||||
color: $body-color; | color: $body-color; | ||||
} | } |
@@ -3,7 +3,7 @@ | |||||
[data-doctype="Web Form"] { | [data-doctype="Web Form"] { | ||||
.page-content-wrapper { | .page-content-wrapper { | ||||
.breadcrumb-container.container { | .breadcrumb-container.container { | ||||
@include media-breakpoint-up(sm) { | @include media-breakpoint-up(sm) { | ||||
padding-left: 0; | padding-left: 0; | ||||
@@ -2,12 +2,14 @@ | |||||
# Copyright (c) 2019, Frappe Technologies and Contributors | # Copyright (c) 2019, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | import frappe | ||||
import unittest | |||||
from frappe.tests.utils import FrappeTestCase | |||||
from .energy_point_log import get_energy_points as _get_energy_points, create_review_points_log, review | from .energy_point_log import get_energy_points as _get_energy_points, create_review_points_log, review | ||||
from frappe.utils.testutils import add_custom_field, clear_custom_fields | from frappe.utils.testutils import add_custom_field, clear_custom_fields | ||||
from frappe.desk.form.assign_to import add as assign_to | from frappe.desk.form.assign_to import add as assign_to | ||||
class TestEnergyPointLog(unittest.TestCase): | |||||
class TestEnergyPointLog(FrappeTestCase): | |||||
@classmethod | @classmethod | ||||
def setUpClass(cls): | def setUpClass(cls): | ||||
settings = frappe.get_single('Energy Point Settings') | settings = frappe.get_single('Energy Point Settings') | ||||
@@ -140,9 +142,10 @@ class TestEnergyPointLog(unittest.TestCase): | |||||
# for criticism | # for criticism | ||||
criticism_points = 2 | criticism_points = 2 | ||||
todo = create_a_todo(description='Bad patch') | |||||
energy_points_before_review = energy_points_after_review | energy_points_before_review = energy_points_after_review | ||||
review_points_before_review = review_points_after_review | review_points_before_review = review_points_after_review | ||||
review(created_todo, criticism_points, 'test@example.com', 'You could have done better.', 'Criticism') | |||||
review(todo, criticism_points, 'test@example.com', 'You could have done better.', 'Criticism') | |||||
energy_points_after_review = get_points('test@example.com') | energy_points_after_review = get_points('test@example.com') | ||||
review_points_after_review = get_points('test2@example.com', 'review_points') | review_points_after_review = get_points('test2@example.com', 'review_points') | ||||
self.assertEqual(energy_points_after_review, energy_points_before_review - criticism_points) | self.assertEqual(energy_points_after_review, energy_points_before_review - criticism_points) | ||||
@@ -332,11 +335,14 @@ def create_energy_point_rule_for_todo(multiplier_field=None, for_doc_event='Cust | |||||
'apply_only_once': apply_once | 'apply_only_once': apply_once | ||||
}).insert(ignore_permissions=1) | }).insert(ignore_permissions=1) | ||||
def create_a_todo(): | |||||
def create_a_todo(description=None): | |||||
if not description: | |||||
description = 'Fix a bug' | |||||
return frappe.get_doc({ | return frappe.get_doc({ | ||||
'doctype': 'ToDo', | 'doctype': 'ToDo', | ||||
'description': 'Fix a bug', | |||||
}).insert() | |||||
'description': description, | |||||
}).insert(ignore_permissions=True) | |||||
def get_points(user, point_type='energy_points'): | def get_points(user, point_type='energy_points'): | ||||
@@ -38,6 +38,6 @@ | |||||
like | like | ||||
} | } | ||||
}); | }); | ||||
} | |||||
} | |||||
}); | }); | ||||
</script> | </script> |
@@ -12,7 +12,11 @@ | |||||
{# powered #} | {# powered #} | ||||
<div class="footer-col-right col-sm-6 col-12 footer-powered"> | <div class="footer-col-right col-sm-6 col-12 footer-powered"> | ||||
{% block powered %} | {% block powered %} | ||||
{% include "templates/includes/footer/footer_powered.html" %} | |||||
{%- if footer_powered -%} | |||||
{{ footer_powered }} | |||||
{%- else -%} | |||||
{% include "templates/includes/footer/footer_powered.html" %} | |||||
{%- endif -%} | |||||
{% endblock %} | {% endblock %} | ||||
</div> | </div> | ||||
</div> | </div> | ||||
@@ -95,8 +95,6 @@ | |||||
min-width: 200px; | min-width: 200px; | ||||
padding: 0px; | padding: 0px; | ||||
font-size: 85%; | font-size: 85%; | ||||
// only rounded bottoms | |||||
border-radius: 0px 0px 4px 4px; | border-radius: 0px 0px 4px 4px; | ||||
} | } | ||||
@@ -11,7 +11,7 @@ | |||||
<p>{{ payment_message or _("Your payment was successfully accepted") }}</p> | <p>{{ payment_message or _("Your payment was successfully accepted") }}</p> | ||||
{% if not payment_message %} | {% if not payment_message %} | ||||
<div> | <div> | ||||
<a | |||||
<a | |||||
href='{{ frappe.form_dict.redirect_to or "/" }}' | href='{{ frappe.form_dict.redirect_to or "/" }}' | ||||
class='btn btn-primary btn-sm'> | class='btn btn-primary btn-sm'> | ||||
{{ _("Continue") }} | {{ _("Continue") }} | ||||
@@ -97,6 +97,12 @@ class TestReportview(unittest.TestCase): | |||||
self.assertFalse(result | self.assertFalse(result | ||||
in DatabaseQuery("DocType").execute(filters={"name": ["not in", 'DocType,DocField']})) | in DatabaseQuery("DocType").execute(filters={"name": ["not in", 'DocType,DocField']})) | ||||
def test_none_filter(self): | |||||
query = frappe.db.query.get_sql("DocType", fields="name", filters={"restrict_to_domain": None}) | |||||
sql = str(query).replace('`', '').replace('"', '') | |||||
condition = 'restrict_to_domain IS NULL' | |||||
self.assertIn(condition, sql) | |||||
def test_or_filters(self): | def test_or_filters(self): | ||||
data = DatabaseQuery("DocField").execute( | data = DatabaseQuery("DocField").execute( | ||||
filters={"parent": "DocType"}, fields=["fieldname", "fieldtype"], | filters={"parent": "DocType"}, fields=["fieldname", "fieldtype"], | ||||
@@ -149,7 +155,6 @@ class TestReportview(unittest.TestCase): | |||||
filters={"creation": ["between", ["2016-07-06", "2016-07-07"]]}, | filters={"creation": ["between", ["2016-07-06", "2016-07-07"]]}, | ||||
fields=["name"]) | fields=["name"]) | ||||
def test_ignore_permissions_for_get_filters_cond(self): | def test_ignore_permissions_for_get_filters_cond(self): | ||||
frappe.set_user('test2@example.com') | frappe.set_user('test2@example.com') | ||||
self.assertRaises(frappe.PermissionError, get_filters_cond, 'DocType', dict(istable=1), []) | self.assertRaises(frappe.PermissionError, get_filters_cond, 'DocType', dict(istable=1), []) | ||||
@@ -351,7 +356,6 @@ class TestReportview(unittest.TestCase): | |||||
self.assertTrue(len(data) == 0) | self.assertTrue(len(data) == 0) | ||||
self.assertTrue(len(frappe.get_all('Nested DocType', {'name': ('not ancestors of', 'Root')})) == len(frappe.get_all('Nested DocType'))) | self.assertTrue(len(frappe.get_all('Nested DocType', {'name': ('not ancestors of', 'Root')})) == len(frappe.get_all('Nested DocType'))) | ||||
def test_is_set_is_not_set(self): | def test_is_set_is_not_set(self): | ||||
res = DatabaseQuery('DocType').execute(filters={'autoname': ['is', 'not set']}) | res = DatabaseQuery('DocType').execute(filters={'autoname': ['is', 'not set']}) | ||||
self.assertTrue({'name': 'Integration Request'} in res) | self.assertTrue({'name': 'Integration Request'} in res) | ||||
@@ -319,3 +319,21 @@ class TestDocument(unittest.TestCase): | |||||
self.assertIsInstance(doc, Note) | self.assertIsInstance(doc, Note) | ||||
self.assertIsInstance(doc.as_dict().get("age"), timedelta) | self.assertIsInstance(doc.as_dict().get("age"), timedelta) | ||||
self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta) | self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta) | ||||
def test_run_method(self): | |||||
doc = frappe.get_last_doc("User") | |||||
# Case 1: Override with a string | |||||
doc.as_dict = "" | |||||
# run_method should throw TypeError | |||||
self.assertRaisesRegex(TypeError, "not callable", doc.run_method, "as_dict") | |||||
# Case 2: Override with a function | |||||
def my_as_dict(*args, **kwargs): | |||||
return "success" | |||||
doc.as_dict = my_as_dict | |||||
# run_method should get overridden | |||||
self.assertEqual(doc.run_method("as_dict"), "success") |
@@ -35,7 +35,7 @@ def get_random(doctype, filters=None, doc=False): | |||||
condition = " where " + " and ".join(condition) | condition = " where " + " and ".join(condition) | ||||
else: | else: | ||||
condition = "" | condition = "" | ||||
out = frappe.db.multisql({ | out = frappe.db.multisql({ | ||||
'mariadb': """select name from `tab%s` %s | 'mariadb': """select name from `tab%s` %s | ||||
order by RAND() limit 1 offset 0""" % (doctype, condition), | order by RAND() limit 1 offset 0""" % (doctype, condition), | ||||
@@ -7,7 +7,7 @@ import re | |||||
import frappe | import frappe | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.utils import get_fullname, date_diff, get_datetime | |||||
from frappe.utils import get_fullname, time_diff_in_hours, get_datetime | |||||
from frappe.utils.user import get_system_managers | from frappe.utils.user import get_system_managers | ||||
from frappe.utils.verified_command import get_signed_params, verify_request | from frappe.utils.verified_command import get_signed_params, verify_request | ||||
import json | import json | ||||
@@ -353,8 +353,8 @@ def process_data_deletion_request(): | |||||
for request in requests: | for request in requests: | ||||
doc = frappe.get_doc("Personal Data Deletion Request", request) | doc = frappe.get_doc("Personal Data Deletion Request", request) | ||||
if date_diff(get_datetime(), doc.creation) >= auto_account_deletion: | |||||
doc.add_comment("Comment", _("The User record for this request has been auto-deleted due to inactivity.")) | |||||
if time_diff_in_hours(get_datetime(), doc.creation) >= auto_account_deletion: | |||||
doc.add_comment("Comment", _("The User record for this request has been auto-deleted due to inactivity by system admins.")) | |||||
doc.trigger_data_deletion() | doc.trigger_data_deletion() | ||||
def remove_unverified_record(): | def remove_unverified_record(): | ||||
@@ -4,10 +4,10 @@ | |||||
import frappe | import frappe | ||||
import unittest | import unittest | ||||
from frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request import ( | from frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request import ( | ||||
remove_unverified_record, | |||||
remove_unverified_record, process_data_deletion_request | |||||
) | ) | ||||
from frappe.website.doctype.personal_data_download_request.test_personal_data_download_request import ( | from frappe.website.doctype.personal_data_download_request.test_personal_data_download_request import ( | ||||
create_user_if_not_exists, | |||||
create_user_if_not_exists | |||||
) | ) | ||||
from datetime import datetime, timedelta | from datetime import datetime, timedelta | ||||
@@ -58,3 +58,15 @@ class TestPersonalDataDeletionRequest(unittest.TestCase): | |||||
self.assertFalse( | self.assertFalse( | ||||
frappe.db.exists("Personal Data Deletion Request", self.delete_request.name) | frappe.db.exists("Personal Data Deletion Request", self.delete_request.name) | ||||
) | ) | ||||
def test_process_auto_request(self): | |||||
frappe.db.set_value("Website Settings", None, "auto_account_deletion", "1") | |||||
date_time_obj = datetime.strptime( | |||||
self.delete_request.creation, "%Y-%m-%d %H:%M:%S.%f" | |||||
) + timedelta(hours=-2) | |||||
self.delete_request.db_set("creation", date_time_obj) | |||||
self.delete_request.db_set("status", "Pending Approval") | |||||
process_data_deletion_request() | |||||
self.delete_request.reload() | |||||
self.assertEqual(self.delete_request.status, "Deleted") |
@@ -42,6 +42,7 @@ | |||||
"copyright", | "copyright", | ||||
"address", | "address", | ||||
"footer_items", | "footer_items", | ||||
"footer_powered", | |||||
"footer_template", | "footer_template", | ||||
"footer_template_values", | "footer_template_values", | ||||
"edit_footer_template_values", | "edit_footer_template_values", | ||||
@@ -142,7 +143,6 @@ | |||||
}, | }, | ||||
{ | { | ||||
"collapsible": 1, | "collapsible": 1, | ||||
"collapsible_depends_on": "top_bar_items", | |||||
"fieldname": "top_bar", | "fieldname": "top_bar", | ||||
"fieldtype": "Section Break", | "fieldtype": "Section Break", | ||||
"label": "Navbar" | "label": "Navbar" | ||||
@@ -189,7 +189,8 @@ | |||||
"description": "Address and other legal information you may want to put in the footer.", | "description": "Address and other legal information you may want to put in the footer.", | ||||
"fieldname": "address", | "fieldname": "address", | ||||
"fieldtype": "Text Editor", | "fieldtype": "Text Editor", | ||||
"label": "Address" | |||||
"label": "Address", | |||||
"max_height": "8rem" | |||||
}, | }, | ||||
{ | { | ||||
"fieldname": "footer_items", | "fieldname": "footer_items", | ||||
@@ -391,6 +392,7 @@ | |||||
"label": "App Logo" | "label": "App Logo" | ||||
}, | }, | ||||
{ | { | ||||
"collapsible": 1, | |||||
"fieldname": "account_deletion_settings_section", | "fieldname": "account_deletion_settings_section", | ||||
"fieldtype": "Section Break", | "fieldtype": "Section Break", | ||||
"label": "Account Deletion Settings" | "label": "Account Deletion Settings" | ||||
@@ -402,10 +404,15 @@ | |||||
"label": "Show Account Deletion Link in My Account Page" | "label": "Show Account Deletion Link in My Account Page" | ||||
}, | }, | ||||
{ | { | ||||
"default": "3", | |||||
"default": "72", | |||||
"fieldname": "auto_account_deletion", | "fieldname": "auto_account_deletion", | ||||
"fieldtype": "Int", | "fieldtype": "Int", | ||||
"label": "Auto Account Deletion within (Days)" | |||||
"label": "Auto Account Deletion within (Hours)" | |||||
}, | |||||
{ | |||||
"fieldname": "footer_powered", | |||||
"fieldtype": "Small Text", | |||||
"label": "Footer \"Powered By\"" | |||||
} | } | ||||
], | ], | ||||
"icon": "fa fa-cog", | "icon": "fa fa-cog", | ||||
@@ -414,7 +421,7 @@ | |||||
"issingle": 1, | "issingle": 1, | ||||
"links": [], | "links": [], | ||||
"max_attachments": 10, | "max_attachments": 10, | ||||
"modified": "2021-12-15 17:28:59.255184", | |||||
"modified": "2022-02-24 15:37:22.360138", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Website", | "module": "Website", | ||||
"name": "Website Settings", | "name": "Website Settings", | ||||
@@ -437,5 +444,6 @@ | |||||
], | ], | ||||
"sort_field": "modified", | "sort_field": "modified", | ||||
"sort_order": "ASC", | "sort_order": "ASC", | ||||
"states": [], | |||||
"track_changes": 1 | "track_changes": 1 | ||||
} | |||||
} |
@@ -120,7 +120,8 @@ def get_website_settings(context=None): | |||||
"facebook_share", "google_plus_one", "twitter_share", "linked_in_share", | "facebook_share", "google_plus_one", "twitter_share", "linked_in_share", | ||||
"disable_signup", "hide_footer_signup", "head_html", "title_prefix", | "disable_signup", "hide_footer_signup", "head_html", "title_prefix", | ||||
"navbar_template", "footer_template", "navbar_search", "enable_view_tracking", | "navbar_template", "footer_template", "navbar_search", "enable_view_tracking", | ||||
"footer_logo", "call_to_action", "call_to_action_url", "show_language_picker"]: | |||||
"footer_logo", "call_to_action", "call_to_action_url", "show_language_picker", | |||||
"footer_powered"]: | |||||
if hasattr(settings, k): | if hasattr(settings, k): | ||||
context[k] = settings.get(k) | context[k] = settings.get(k) | ||||
@@ -5,7 +5,7 @@ frappe.ready(function() { | |||||
callback: (data) => { | callback: (data) => { | ||||
if (data.message) { | if (data.message) { | ||||
const intro_wrapper = $('#introduction .ql-editor.read-mode'); | const intro_wrapper = $('#introduction .ql-editor.read-mode'); | ||||
const sla_description = __("Note: Your request for account deletion will be fulfilled within {0} days.", [data.message]); | |||||
const sla_description = __("Note: Your request for account deletion will be fulfilled within {0} hours.", [data.message]); | |||||
const sla_description_wrapper = `<br><b>${sla_description}</b>`; | const sla_description_wrapper = `<br><b>${sla_description}</b>`; | ||||
intro_wrapper.html(intro_wrapper.html() + sla_description_wrapper); | intro_wrapper.html(intro_wrapper.html() + sla_description_wrapper); | ||||
} | } | ||||
@@ -5,6 +5,7 @@ import unittest | |||||
from frappe.utils import random_string | from frappe.utils import random_string | ||||
from frappe.model.workflow import apply_workflow, WorkflowTransitionError, WorkflowPermissionError, get_common_transition_actions | from frappe.model.workflow import apply_workflow, WorkflowTransitionError, WorkflowPermissionError, get_common_transition_actions | ||||
from frappe.test_runner import make_test_records | from frappe.test_runner import make_test_records | ||||
from frappe.query_builder import DocType | |||||
class TestWorkflow(unittest.TestCase): | class TestWorkflow(unittest.TestCase): | ||||
@@ -15,9 +16,31 @@ class TestWorkflow(unittest.TestCase): | |||||
def setUp(self): | def setUp(self): | ||||
self.workflow = create_todo_workflow() | self.workflow = create_todo_workflow() | ||||
frappe.set_user('Administrator') | frappe.set_user('Administrator') | ||||
if self._testMethodName == "test_if_workflow_actions_were_processed_using_user": | |||||
if not frappe.db.has_column('Workflow Action', 'user'): | |||||
# mariadb would raise this statement would create an implicit commit | |||||
# if we do not commit before alter statement | |||||
# nosemgrep | |||||
frappe.db.commit() | |||||
frappe.db.multisql({ | |||||
'mariadb': 'ALTER TABLE `tabWorkflow Action` ADD COLUMN user varchar(140)', | |||||
'postgres': 'ALTER TABLE "tabWorkflow Action" ADD COLUMN "user" varchar(140)' | |||||
}) | |||||
frappe.cache().delete_value('table_columns') | |||||
def tearDown(self): | def tearDown(self): | ||||
frappe.delete_doc('Workflow', 'Test ToDo') | frappe.delete_doc('Workflow', 'Test ToDo') | ||||
if self._testMethodName == "test_if_workflow_actions_were_processed_using_user": | |||||
if frappe.db.has_column('Workflow Action', 'user'): | |||||
# mariadb would raise this statement would create an implicit commit | |||||
# if we do not commit before alter statement | |||||
# nosemgrep | |||||
frappe.db.commit() | |||||
frappe.db.multisql({ | |||||
'mariadb': 'ALTER TABLE `tabWorkflow Action` DROP COLUMN user', | |||||
'postgres': 'ALTER TABLE "tabWorkflow Action" DROP COLUMN "user"' | |||||
}) | |||||
frappe.cache().delete_value('table_columns') | |||||
def test_default_condition(self): | def test_default_condition(self): | ||||
'''test default condition is set''' | '''test default condition is set''' | ||||
@@ -75,7 +98,7 @@ class TestWorkflow(unittest.TestCase): | |||||
actions = get_common_transition_actions([todo1, todo2], 'ToDo') | actions = get_common_transition_actions([todo1, todo2], 'ToDo') | ||||
self.assertListEqual(actions, ['Review']) | self.assertListEqual(actions, ['Review']) | ||||
def test_if_workflow_actions_were_processed(self): | |||||
def test_if_workflow_actions_were_processed_using_role(self): | |||||
frappe.db.delete("Workflow Action") | frappe.db.delete("Workflow Action") | ||||
user = frappe.get_doc('User', 'test2@example.com') | user = frappe.get_doc('User', 'test2@example.com') | ||||
user.add_roles('Test Approver', 'System Manager') | user.add_roles('Test Approver', 'System Manager') | ||||
@@ -93,6 +116,32 @@ class TestWorkflow(unittest.TestCase): | |||||
self.assertEqual(workflow_actions[0].status, 'Completed') | self.assertEqual(workflow_actions[0].status, 'Completed') | ||||
frappe.set_user('Administrator') | frappe.set_user('Administrator') | ||||
def test_if_workflow_actions_were_processed_using_user(self): | |||||
frappe.db.delete("Workflow Action") | |||||
user = frappe.get_doc('User', 'test2@example.com') | |||||
user.add_roles('Test Approver', 'System Manager') | |||||
frappe.set_user('test2@example.com') | |||||
doc = self.test_default_condition() | |||||
workflow_actions = frappe.get_all('Workflow Action', fields=['*']) | |||||
self.assertEqual(len(workflow_actions), 1) | |||||
# test if status of workflow actions are updated on approval | |||||
WorkflowAction = DocType("Workflow Action") | |||||
WorkflowActionPermittedRole = DocType("Workflow Action Permitted Role") | |||||
frappe.qb.update(WorkflowAction).set(WorkflowAction.user, 'test2@example.com').run() | |||||
frappe.qb.update(WorkflowActionPermittedRole).set(WorkflowActionPermittedRole.role, '').run() | |||||
self.test_approve(doc) | |||||
user.remove_roles('Test Approver', 'System Manager') | |||||
workflow_actions = frappe.get_all('Workflow Action', fields=['status']) | |||||
self.assertEqual(len(workflow_actions), 1) | |||||
self.assertEqual(workflow_actions[0].status, 'Completed') | |||||
frappe.set_user('Administrator') | |||||
def test_update_docstatus(self): | def test_update_docstatus(self): | ||||
todo = create_new_todo() | todo = create_new_todo() | ||||
apply_workflow(todo, 'Approve') | apply_workflow(todo, 'Approve') | ||||
@@ -8,9 +8,12 @@ | |||||
"status", | "status", | ||||
"reference_name", | "reference_name", | ||||
"reference_doctype", | "reference_doctype", | ||||
"user", | |||||
"workflow_state", | "workflow_state", | ||||
"completed_by" | |||||
"column_break_5", | |||||
"completed_by_role", | |||||
"completed_by", | |||||
"permitted_roles", | |||||
"user" | |||||
], | ], | ||||
"fields": [ | "fields": [ | ||||
{ | { | ||||
@@ -24,16 +27,14 @@ | |||||
"fieldname": "reference_name", | "fieldname": "reference_name", | ||||
"fieldtype": "Dynamic Link", | "fieldtype": "Dynamic Link", | ||||
"label": "Reference Name", | "label": "Reference Name", | ||||
"options": "reference_doctype", | |||||
"search_index": 1 | |||||
"options": "reference_doctype" | |||||
}, | }, | ||||
{ | { | ||||
"fieldname": "reference_doctype", | "fieldname": "reference_doctype", | ||||
"fieldtype": "Link", | "fieldtype": "Link", | ||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"label": "Reference Document Type", | "label": "Reference Document Type", | ||||
"options": "DocType", | |||||
"search_index": 1 | |||||
"options": "DocType" | |||||
}, | }, | ||||
{ | { | ||||
"fieldname": "user", | "fieldname": "user", | ||||
@@ -47,18 +48,38 @@ | |||||
"fieldname": "workflow_state", | "fieldname": "workflow_state", | ||||
"fieldtype": "Data", | "fieldtype": "Data", | ||||
"hidden": 1, | "hidden": 1, | ||||
"label": "Workflow State", | |||||
"search_index": 1 | |||||
"label": "Workflow State" | |||||
}, | }, | ||||
{ | { | ||||
"depends_on": "eval: doc.completed_by", | |||||
"fieldname": "completed_by", | "fieldname": "completed_by", | ||||
"fieldtype": "Link", | "fieldtype": "Link", | ||||
"label": "Completed By", | |||||
"options": "User" | |||||
"label": "Completed By User", | |||||
"options": "User", | |||||
"read_only": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "column_break_5", | |||||
"fieldtype": "Column Break" | |||||
}, | |||||
{ | |||||
"depends_on": "eval: doc.completed_by_role", | |||||
"fieldname": "completed_by_role", | |||||
"fieldtype": "Link", | |||||
"label": "Completed By Role", | |||||
"options": "Role", | |||||
"read_only": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "permitted_roles", | |||||
"fieldtype": "Table MultiSelect", | |||||
"label": "Permitted Roles", | |||||
"options": "Workflow Action Permitted Role", | |||||
"read_only": 1 | |||||
} | } | ||||
], | ], | ||||
"links": [], | "links": [], | ||||
"modified": "2021-07-01 09:07:52.848618", | |||||
"modified": "2022-02-23 21:06:45.122258", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Workflow", | "module": "Workflow", | ||||
"name": "Workflow Action", | "name": "Workflow Action", | ||||
@@ -72,6 +93,7 @@ | |||||
], | ], | ||||
"sort_field": "modified", | "sort_field": "modified", | ||||
"sort_order": "DESC", | "sort_order": "DESC", | ||||
"states": [], | |||||
"title_field": "reference_name", | "title_field": "reference_name", | ||||
"track_changes": 1 | "track_changes": 1 | ||||
} | } |
@@ -13,24 +13,46 @@ from frappe.model.workflow import apply_workflow, get_workflow_name, has_approva | |||||
from frappe.desk.notifications import clear_doctype_notifications | from frappe.desk.notifications import clear_doctype_notifications | ||||
from frappe.utils.user import get_users_with_role | from frappe.utils.user import get_users_with_role | ||||
from frappe.utils.data import get_link_to_form | from frappe.utils.data import get_link_to_form | ||||
from frappe.query_builder import DocType | |||||
class WorkflowAction(Document): | class WorkflowAction(Document): | ||||
pass | pass | ||||
def on_doctype_update(): | def on_doctype_update(): | ||||
frappe.db.add_index("Workflow Action", ["status", "user"]) | |||||
# The search order in any use case is no ["reference_name", "reference_doctype", "status"] | |||||
# The index scan would happen from left to right | |||||
# so even if status is not in the where clause the index will be used | |||||
frappe.db.add_index("Workflow Action", ["reference_name", "reference_doctype", "status"]) | |||||
def get_permission_query_conditions(user): | def get_permission_query_conditions(user): | ||||
if not user: user = frappe.session.user | if not user: user = frappe.session.user | ||||
if user == "Administrator": return "" | if user == "Administrator": return "" | ||||
return "(`tabWorkflow Action`.`user`='{user}')".format(user=user) | |||||
roles = frappe.get_roles(user) | |||||
WorkflowAction = DocType("Workflow Action") | |||||
WorkflowActionPermittedRole = DocType("Workflow Action Permitted Role") | |||||
permitted_workflow_actions = (frappe.qb.from_(WorkflowAction) | |||||
.join(WorkflowActionPermittedRole) | |||||
.on(WorkflowAction.name == WorkflowActionPermittedRole.parent) | |||||
.select(WorkflowAction.name) | |||||
.where(WorkflowActionPermittedRole.role.isin(roles)) | |||||
).get_sql() | |||||
return f"""(`tabWorkflow Action`.`name` in ({permitted_workflow_actions}) | |||||
or `tabWorkflow Action`.`user`='{user}') | |||||
and `tabWorkflow Action`.`status`='Open'""" | |||||
def has_permission(doc, user): | def has_permission(doc, user): | ||||
if user not in ['Administrator', doc.user]: | |||||
return False | |||||
user_roles = set(frappe.get_roles(user)) | |||||
permitted_roles = {permitted_role.role for permitted_role in doc.permitted_roles} | |||||
return user == "Administrator" or user_roles.intersection(permitted_roles) | |||||
def process_workflow_actions(doc, state): | def process_workflow_actions(doc, state): | ||||
workflow = get_workflow_name(doc.get('doctype')) | workflow = get_workflow_name(doc.get('doctype')) | ||||
@@ -42,19 +64,18 @@ def process_workflow_actions(doc, state): | |||||
if is_workflow_action_already_created(doc): return | if is_workflow_action_already_created(doc): return | ||||
clear_old_workflow_actions(doc) | |||||
update_completed_workflow_actions(doc) | |||||
update_completed_workflow_actions(doc, workflow=workflow, workflow_state=get_doc_workflow_state(doc)) | |||||
clear_doctype_notifications('Workflow Action') | clear_doctype_notifications('Workflow Action') | ||||
next_possible_transitions = get_next_possible_transitions(workflow, get_doc_workflow_state(doc), doc) | next_possible_transitions = get_next_possible_transitions(workflow, get_doc_workflow_state(doc), doc) | ||||
if not next_possible_transitions: return | if not next_possible_transitions: return | ||||
user_data_map = get_users_next_action_data(next_possible_transitions, doc) | |||||
user_data_map, roles = get_users_next_action_data(next_possible_transitions, doc) | |||||
if not user_data_map: return | if not user_data_map: return | ||||
create_workflow_actions_for_users(user_data_map.keys(), doc) | |||||
create_workflow_actions_for_roles(roles, doc) | |||||
if send_email_alert(workflow): | if send_email_alert(workflow): | ||||
enqueue(send_workflow_action_email, queue='short', users_data=list(user_data_map.values()), doc=doc) | enqueue(send_workflow_action_email, queue='short', users_data=list(user_data_map.values()), doc=doc) | ||||
@@ -132,20 +153,85 @@ def return_link_expired_page(doc, doc_workflow_state): | |||||
frappe.bold(frappe.get_value('User', doc.get("modified_by"), 'full_name')) | frappe.bold(frappe.get_value('User', doc.get("modified_by"), 'full_name')) | ||||
), indicator_color='blue') | ), indicator_color='blue') | ||||
def clear_old_workflow_actions(doc, user=None): | |||||
def update_completed_workflow_actions(doc, user=None, workflow=None, workflow_state=None): | |||||
allowed_roles = get_allowed_roles(user, workflow, workflow_state) | |||||
# There is no transaction leading upto this state | |||||
# so no older actions to complete | |||||
if not allowed_roles: | |||||
return | |||||
if workflow_action := get_workflow_action_by_role(doc, allowed_roles): | |||||
update_completed_workflow_actions_using_role(doc, user, allowed_roles, workflow_action) | |||||
else: | |||||
# backwards compatibility | |||||
# for workflow actions saved using user | |||||
clear_old_workflow_actions_using_user(doc, user) | |||||
update_completed_workflow_actions_using_user(doc, user) | |||||
def get_allowed_roles(user, workflow, workflow_state): | |||||
user = user if user else frappe.session.user | user = user if user else frappe.session.user | ||||
frappe.db.delete("Workflow Action", { | |||||
"reference_doctype": doc.get("doctype"), | |||||
"reference_name": doc.get("name"), | |||||
"user": ("!=", user), | |||||
"status": "Open" | |||||
}) | |||||
def update_completed_workflow_actions(doc, user=None): | |||||
allowed_roles = frappe.get_all('Workflow Transition', | |||||
fields='allowed', | |||||
filters=[ | |||||
['parent', '=', workflow], | |||||
['next_state', '=', workflow_state] | |||||
], | |||||
pluck = 'allowed') | |||||
user_roles = set(frappe.get_roles(user)) | |||||
return set(allowed_roles).intersection(user_roles) | |||||
def get_workflow_action_by_role(doc, allowed_roles): | |||||
WorkflowAction = DocType("Workflow Action") | |||||
WorkflowActionPermittedRole = DocType("Workflow Action Permitted Role") | |||||
return (frappe.qb.from_(WorkflowAction).join(WorkflowActionPermittedRole) | |||||
.on(WorkflowAction.name == WorkflowActionPermittedRole.parent) | |||||
.select(WorkflowAction.name, WorkflowActionPermittedRole.role) | |||||
.where((WorkflowAction.reference_name == doc.get('name')) | |||||
& (WorkflowAction.reference_doctype == doc.get('doctype')) | |||||
& (WorkflowAction.status == 'Open') | |||||
& (WorkflowActionPermittedRole.role.isin(list(allowed_roles)))) | |||||
.orderby(WorkflowActionPermittedRole.role).limit(1)).run(as_dict=True) | |||||
def update_completed_workflow_actions_using_role(doc, user=None, allowed_roles = set(), workflow_action=None): | |||||
user = user if user else frappe.session.user | user = user if user else frappe.session.user | ||||
frappe.db.sql("""UPDATE `tabWorkflow Action` SET `status`='Completed', `completed_by`=%s | |||||
WHERE `reference_doctype`=%s AND `reference_name`=%s AND `user`=%s AND `status`='Open'""", | |||||
(user, doc.get('doctype'), doc.get('name'), user)) | |||||
WorkflowAction = DocType("Workflow Action") | |||||
if not workflow_action: | |||||
return | |||||
(frappe.qb.update(WorkflowAction) | |||||
.set(WorkflowAction.status, 'Completed') | |||||
.set(WorkflowAction.completed_by, user) | |||||
.set(WorkflowAction.completed_by_role, workflow_action[0].role) | |||||
.where(WorkflowAction.name == workflow_action[0].name) | |||||
).run() | |||||
def clear_old_workflow_actions_using_user(doc, user=None): | |||||
user = user if user else frappe.session.user | |||||
if frappe.db.has_column('Workflow Action', 'user'): | |||||
frappe.db.delete("Workflow Action", { | |||||
"reference_name": doc.get("name"), | |||||
"reference_doctype": doc.get("doctype"), | |||||
"status": "Open", | |||||
"user": ("!=", user) | |||||
}) | |||||
def update_completed_workflow_actions_using_user(doc, user=None): | |||||
user = user or frappe.session.user | |||||
if frappe.db.has_column('Workflow Action', 'user'): | |||||
WorkflowAction = DocType("Workflow Action") | |||||
(frappe.qb.update(WorkflowAction) | |||||
.set(WorkflowAction.status, 'Completed') | |||||
.set(WorkflowAction.completed_by, user) | |||||
.where((WorkflowAction.reference_name == doc.get('name')) | |||||
& (WorkflowAction.reference_doctype == doc.get('doctype')) | |||||
& (WorkflowAction.status == 'Open') | |||||
& (WorkflowAction.user == user)) | |||||
).run() | |||||
def get_next_possible_transitions(workflow_name, state, doc=None): | def get_next_possible_transitions(workflow_name, state, doc=None): | ||||
transitions = frappe.get_all('Workflow Transition', | transitions = frappe.get_all('Workflow Transition', | ||||
@@ -167,8 +253,10 @@ def get_next_possible_transitions(workflow_name, state, doc=None): | |||||
return transitions_to_return | return transitions_to_return | ||||
def get_users_next_action_data(transitions, doc): | def get_users_next_action_data(transitions, doc): | ||||
roles = set() | |||||
user_data_map = {} | user_data_map = {} | ||||
for transition in transitions: | for transition in transitions: | ||||
roles.add(transition.allowed) | |||||
users = get_users_with_role(transition.allowed) | users = get_users_with_role(transition.allowed) | ||||
filtered_users = filter_allowed_users(users, doc, transition) | filtered_users = filter_allowed_users(users, doc, transition) | ||||
for user in filtered_users: | for user in filtered_users: | ||||
@@ -182,19 +270,24 @@ def get_users_next_action_data(transitions, doc): | |||||
'action_name': transition.action, | 'action_name': transition.action, | ||||
'action_link': get_workflow_action_url(transition.action, doc, user) | 'action_link': get_workflow_action_url(transition.action, doc, user) | ||||
})) | })) | ||||
return user_data_map | |||||
return user_data_map, roles | |||||
def create_workflow_actions_for_users(users, doc): | |||||
for user in users: | |||||
frappe.get_doc({ | |||||
'doctype': 'Workflow Action', | |||||
'reference_doctype': doc.get('doctype'), | |||||
'reference_name': doc.get('name'), | |||||
'workflow_state': get_doc_workflow_state(doc), | |||||
'status': 'Open', | |||||
'user': user | |||||
}).insert(ignore_permissions=True) | |||||
def create_workflow_actions_for_roles(roles, doc): | |||||
workflow_action = frappe.get_doc({ | |||||
'doctype': 'Workflow Action', | |||||
'reference_doctype': doc.get('doctype'), | |||||
'reference_name': doc.get('name'), | |||||
'workflow_state': get_doc_workflow_state(doc), | |||||
'status': 'Open', | |||||
}) | |||||
for role in roles: | |||||
workflow_action.append('permitted_roles', { | |||||
'role': role | |||||
}) | |||||
workflow_action.insert(ignore_permissions=True) | |||||
def send_workflow_action_email(users_data, doc): | def send_workflow_action_email(users_data, doc): | ||||
common_args = get_common_email_args(doc) | common_args = get_common_email_args(doc) | ||||
@@ -249,18 +342,20 @@ def get_confirm_workflow_action_url(doc, action, user): | |||||
def is_workflow_action_already_created(doc): | def is_workflow_action_already_created(doc): | ||||
return frappe.db.exists({ | return frappe.db.exists({ | ||||
'doctype': 'Workflow Action', | 'doctype': 'Workflow Action', | ||||
'reference_doctype': doc.get('doctype'), | |||||
'reference_name': doc.get('name'), | 'reference_name': doc.get('name'), | ||||
'workflow_state': get_doc_workflow_state(doc) | |||||
'reference_doctype': doc.get('doctype'), | |||||
'workflow_state': get_doc_workflow_state(doc), | |||||
}) | }) | ||||
def clear_workflow_actions(doctype, name): | def clear_workflow_actions(doctype, name): | ||||
if not (doctype and name): | if not (doctype and name): | ||||
return | return | ||||
frappe.db.delete("Workflow Action", { | |||||
"reference_doctype": doctype, | |||||
"reference_name": name | |||||
}) | |||||
frappe.db.delete("Workflow Action", filters = { | |||||
"reference_name": name, | |||||
"reference_doctype": doctype, | |||||
} | |||||
) | |||||
def get_doc_workflow_state(doc): | def get_doc_workflow_state(doc): | ||||
workflow_name = get_workflow_name(doc.get('doctype')) | workflow_name = get_workflow_name(doc.get('doctype')) | ||||
workflow_state_field = get_workflow_state_field(workflow_name) | workflow_state_field = get_workflow_state_field(workflow_name) | ||||
@@ -0,0 +1,33 @@ | |||||
{ | |||||
"actions": [], | |||||
"allow_rename": 1, | |||||
"autoname": "hash", | |||||
"creation": "2022-02-21 20:28:05.662187", | |||||
"doctype": "DocType", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"field_order": [ | |||||
"role" | |||||
], | |||||
"fields": [ | |||||
{ | |||||
"fieldname": "role", | |||||
"fieldtype": "Link", | |||||
"label": "Role", | |||||
"options": "Role" | |||||
} | |||||
], | |||||
"index_web_pages_for_search": 1, | |||||
"istable": 1, | |||||
"links": [], | |||||
"modified": "2022-02-21 20:28:05.662187", | |||||
"modified_by": "Administrator", | |||||
"module": "Workflow", | |||||
"name": "Workflow Action Permitted Role", | |||||
"naming_rule": "Random", | |||||
"owner": "Administrator", | |||||
"permissions": [], | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"states": [] | |||||
} |
@@ -0,0 +1,8 @@ | |||||
# Copyright (c) 2022, Frappe Technologies and contributors | |||||
# For license information, please see license.txt | |||||
# import frappe | |||||
from frappe.model.document import Document | |||||
class WorkflowActionPermittedRole(Document): | |||||
pass |
@@ -22,7 +22,7 @@ | |||||
add_top_padding=1, | add_top_padding=1, | ||||
add_bottom_padding=1, | add_bottom_padding=1, | ||||
) }} | ) }} | ||||
{% if doc.get({"doctype":"Company History"}) %} | {% if doc.get({"doctype":"Company History"}) %} | ||||
<section class="section section-padding-bottom"> | <section class="section section-padding-bottom"> | ||||
@@ -87,7 +87,7 @@ | |||||
{% if item.target %}target="{{ item.target }}"{% endif %}> | {% if item.target %}target="{{ item.target }}"{% endif %}> | ||||
{{ _(item.title or item.label) }} | {{ _(item.title or item.label) }} | ||||
</a> | </a> | ||||
{%- endfor %} | |||||
{%- endfor %} | |||||
</ul> | </ul> | ||||
</div> | </div> | ||||
</div> | </div> |
@@ -10,6 +10,6 @@ no_cache = 1 | |||||
def get_context(context): | def get_context(context): | ||||
if frappe.session.user=='Guest': | if frappe.session.user=='Guest': | ||||
frappe.throw(_("You need to be logged in to access this page"), frappe.PermissionError) | frappe.throw(_("You need to be logged in to access this page"), frappe.PermissionError) | ||||
context.current_user = frappe.get_doc("User", frappe.session.user) | context.current_user = frappe.get_doc("User", frappe.session.user) | ||||
context.show_sidebar=True | context.show_sidebar=True |
@@ -53,8 +53,8 @@ | |||||
{% endfor %} | {% endfor %} | ||||
{% else %} | {% else %} | ||||
<div class="empty-apps-state"> | <div class="empty-apps-state"> | ||||
<img src="/assets/frappe/images/ui-states/empty-app-state.svg"/> | |||||
<div class="font-weight-bold mt-4"> | |||||
<img src="/assets/frappe/images/ui-states/empty-app-state.svg"/> | |||||
<div class="font-weight-bold mt-4"> | |||||
{{ _("No Active Sessions")}} | {{ _("No Active Sessions")}} | ||||
</div> | </div> | ||||
<div class="text-muted mt-2"> | <div class="text-muted mt-2"> | ||||