@@ -16,3 +16,6 @@ fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85 | |||
# Refactor "not a in b" -> "a not in b" | |||
745297a49d516e5e3c4bb3e1b0c4235e7d31165d | |||
# Clean up whitespace | |||
b2fc959307c7c79f5584625569d5aed04133ba13 |
@@ -1,15 +1,24 @@ | |||
name: Semgrep | |||
name: Linters | |||
on: | |||
pull_request: { } | |||
jobs: | |||
semgrep: | |||
linters: | |||
name: Frappe Linter | |||
runs-on: ubuntu-latest | |||
steps: | |||
- 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 | |||
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: | |||
status: | |||
patch: off | |||
project: | |||
default: false | |||
server: | |||
@@ -166,7 +166,7 @@ class Importer: | |||
if not self.data_import.status == "Partial Success": | |||
self.data_import.db_set("status", "Partial Success") | |||
# commit after every successful import | |||
frappe.db.commit() | |||
@@ -2,7 +2,8 @@ | |||
# See license.txt | |||
# import frappe | |||
import unittest | |||
from frappe.tests.utils import FrappeTestCase | |||
class Test{classname}(unittest.TestCase): | |||
class Test{classname}(FrappeTestCase): | |||
pass |
@@ -314,19 +314,19 @@ result = [ | |||
{ | |||
"parent_column": "Parent 1", | |||
"column_1": 200, | |||
"column_2": 150.50 | |||
"column_2": 150.50 | |||
}, | |||
{ | |||
"parent_column": "Child 1", | |||
"column_1": 100, | |||
"column_2": 75.25, | |||
"parent_value": "Parent 1" | |||
"parent_value": "Parent 1" | |||
}, | |||
{ | |||
"parent_column": "Child 2", | |||
"column_1": 100, | |||
"column_2": 75.25, | |||
"parent_value": "Parent 1" | |||
"parent_value": "Parent 1" | |||
} | |||
] | |||
@@ -244,7 +244,13 @@ class Query: | |||
_operator = OPERATOR_MAP[value[0]] | |||
conditions = conditions.where(_operator(Field(key), value[1])) | |||
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) | |||
return conditions | |||
@@ -495,7 +495,7 @@ frappe.ui.form.on('Dashboard Chart', { | |||
set_parent_document_type: async function(frm) { | |||
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; | |||
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 () => { | |||
const issingle = await check_if_single(frm.doc.reference_doctype); | |||
let route_changed = null; | |||
if (issingle) { | |||
route_changed = frappe.set_route('Form', frm.doc.reference_doctype); | |||
} else if (frm.doc.first_document) { | |||
@@ -76,26 +76,6 @@ def archive_restore_column(board_name, column_title, status): | |||
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() | |||
def update_order(board_name, order): | |||
'''Save the order of cards in columns''' | |||
@@ -24,7 +24,7 @@ frappe.views.calendar["ToDo"] = { | |||
"options": "reference_type", | |||
"label": __("Task") | |||
} | |||
], | |||
get_events_method: "frappe.desk.calendar.get_events" | |||
}; | |||
@@ -9,7 +9,7 @@ frappe.ui.form.on('Workspace', { | |||
refresh: function(frm) { | |||
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'))) { | |||
frm.trigger('disable_form'); | |||
} | |||
@@ -176,9 +176,9 @@ def update_page(name, title, icon, parent, public): | |||
doc = frappe.get_doc("Workspace", name) | |||
filters = { | |||
filters = { | |||
'parent_page': doc.title, | |||
'public': doc.public | |||
'public': doc.public | |||
} | |||
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): | |||
if not loads(sb_public_items) and not loads(sb_private_items): | |||
return | |||
sb_public_items = loads(sb_public_items) | |||
sb_private_items = loads(sb_private_items) | |||
@@ -292,7 +292,7 @@ def last_sequence_id(doc): | |||
if not doc_exists: | |||
return 0 | |||
return frappe.db.get_list('Workspace', | |||
return frappe.db.get_list('Workspace', | |||
fields=['sequence_id'], | |||
filters={ | |||
'public': doc.public, | |||
@@ -406,7 +406,7 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visi | |||
for column in data.columns: | |||
if column.get("hidden"): | |||
continue | |||
result[0].append(column.get("label")) | |||
result[0].append(_(column.get("label"))) | |||
column_width = cint(column.get('width', 0)) | |||
# to convert into scale accepted by openpyxl | |||
column_width /= 10 | |||
@@ -61,7 +61,7 @@ def get_context(context): | |||
""") | |||
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')) | |||
def validate_condition(self): | |||
@@ -630,7 +630,7 @@ class InboundMail(Email): | |||
if self.reference_document(): | |||
data['reference_doctype'] = self.reference_document().doctype | |||
data['reference_name'] = self.reference_document().name | |||
else: | |||
else: | |||
if append_to and append_to != 'Communication': | |||
reference_doc = self._create_reference_document(append_to) | |||
if reference_doc: | |||
@@ -221,7 +221,8 @@ scheduler_events = { | |||
"frappe.deferred_insert.save_to_db", | |||
"frappe.desk.form.document_follow.send_hourly_updates", | |||
"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": [ | |||
"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.email.doctype.unhandled_email.unhandled_email.remove_old_unhandled_emails", | |||
"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": [ | |||
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily", | |||
@@ -3,6 +3,6 @@ | |||
frappe.ui.form.on('Razorpay Settings', { | |||
refresh: function(frm) { | |||
} | |||
}); |
@@ -471,7 +471,7 @@ class Document(BaseDocument): | |||
# 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 | |||
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.owner = self.modified_by | |||
@@ -860,14 +860,14 @@ class Document(BaseDocument): | |||
def run_method(self, method, *args, **kwargs): | |||
"""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) | |||
out = Document.hook(fn)(self, *args, **kwargs) | |||
@@ -1003,8 +1003,6 @@ class Document(BaseDocument): | |||
- `on_cancel` for **Cancel** | |||
- `update_after_submit` for **Update after Submit**""" | |||
doc_before_save = self.get_doc_before_save() | |||
if self._action=="save": | |||
self.run_method("on_update") | |||
elif self._action=="submit": | |||
@@ -77,13 +77,15 @@ def rename_doc( | |||
) -> str: | |||
"""Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link".""" | |||
if not frappe.db.exists(doctype, old): | |||
frappe.errprint(_("Failed: {0} to {1} because {0} doesn't exist.").format(old, new)) | |||
return | |||
if ignore_if_exists and frappe.db.exists(doctype, new): | |||
frappe.errprint(_("Failed: {0} to {1} because {1} already exists.").format(old, new)) | |||
return | |||
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 | |||
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]) | |||
frappe.db.commit() | |||
else: | |||
msg = _("Ignored: {0} to {1}").format(row[0], row[1]) | |||
msg = None | |||
except Exception as e: | |||
msg = _("** Failed: {0} to {1}: {2}").format(row[0], row[1], repr(e)) | |||
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) | |||
@@ -37,6 +37,7 @@ patches by using INI like file format: | |||
import configparser | |||
import time | |||
from enum import Enum | |||
from textwrap import dedent, indent | |||
from typing import List, Optional | |||
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): | |||
"""execute the patch""" | |||
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() | |||
frappe.db.begin() | |||
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.startswith("finally:"): | |||
# run run patch at the end | |||
frappe.flags.final_patches.append(patchmodule) | |||
else: | |||
if patchmodule.startswith("execute:"): | |||
exec(patchmodule.split("execute:")[1],globals()) | |||
if has_patch_file: | |||
_patch() | |||
else: | |||
frappe.get_attr(patchmodule.split()[0] + ".execute")() | |||
exec(patch, globals()) | |||
update_patch_log(patchmodule) | |||
elif method: | |||
method(**methodargs) | |||
@@ -174,7 +190,7 @@ def execute_patch(patchmodule, method=None, methodargs=None): | |||
frappe.db.commit() | |||
end_time = time.time() | |||
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 | |||
@@ -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.transform_todo_schema | |||
frappe.patches.v14_0.remove_post_and_post_comment | |||
frappe.patches.v14_0.reset_creation_datetime | |||
[post_model_sync] | |||
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.remove_db_aggregation | |||
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: | |||
file_path = file.file_url | |||
file_name = file_path.split('/')[-1] | |||
if not file_path.startswith(('/private/', '/files/')): | |||
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() { | |||
let df_options = this.df.options; | |||
if (!df_options) return {}; | |||
let options = {}; | |||
if (typeof df_options === 'string') { | |||
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)) { | |||
return false; | |||
} | |||
const is_field_matching = () => { | |||
return ( | |||
field.fieldname.toLowerCase() === field_name || | |||
@@ -88,6 +88,9 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for | |||
make_quill_editor() { | |||
if (this.quill) return; | |||
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.bind_events(); | |||
} | |||
@@ -66,7 +66,7 @@ export default class GridPagination { | |||
} | |||
// 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)) { | |||
return false; | |||
} | |||
@@ -615,6 +615,7 @@ export default class GridRow { | |||
if (!this.doc) { | |||
$col.attr("title", txt); | |||
} | |||
df.fieldname && $col.static_area.toggleClass('reqd', Boolean(df.reqd)); | |||
$col.df = df; | |||
$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) { | |||
has_errors = true; | |||
error_fields = [__('Name'), ...error_fields]; | |||
} | |||
@@ -103,7 +103,9 @@ frappe.ui.form.Toolbar = class Toolbar { | |||
docname, | |||
name: new_name, | |||
title: new_title, | |||
merge | |||
merge, | |||
freeze: true, | |||
freeze_message: __("Updating related fields...") | |||
}).then(new_docname => { | |||
if (new_name != docname) { | |||
$(document).trigger("rename", [doctype, docname, new_docname || new_name]); | |||
@@ -172,6 +174,7 @@ frappe.ui.form.Toolbar = class Toolbar { | |||
d.show(); | |||
d.set_primary_action(__("Rename"), (values) => { | |||
d.disable_primary_action(); | |||
d.hide(); | |||
this.rename_document_title(values.name, values.title, values.merge) | |||
.then(() => { | |||
d.hide(); | |||
@@ -150,7 +150,7 @@ frappe.views.ListViewSelect = class ListViewSelect { | |||
const views_wrapper = this.sidebar.sidebar.find(".views-section"); | |||
views_wrapper.find(".sidebar-label").html(`${__(view)}`); | |||
const $dropdown = views_wrapper.find(".views-dropdown"); | |||
let placeholder = `${__("Select {0}", [__(view)])}`; | |||
let html = ``; | |||
@@ -615,10 +615,13 @@ $.extend(frappe.model, { | |||
}); | |||
d.set_primary_action(__("Rename"), function() { | |||
d.hide(); | |||
var args = d.get_values(); | |||
if(!args) return; | |||
return frappe.call({ | |||
method:"frappe.rename_doc", | |||
freeze: true, | |||
freeze_message: "Updating related fields...", | |||
args: { | |||
doctype: doctype, | |||
old: docname, | |||
@@ -50,6 +50,11 @@ frappe.call = function(opts) { | |||
} | |||
var args = $.extend({}, opts.args); | |||
if (args.freeze) { | |||
opts.freeze = opts.freeze || args.freeze; | |||
opts.freeze_message = opts.freeze_message || args.freeze_message; | |||
} | |||
// cmd | |||
if(opts.module && opts.page) { | |||
args.cmd = opts.module+'.page.'+opts.page+'.'+opts.page+'.'+opts.method; | |||
@@ -112,9 +112,9 @@ frappe.ui.FieldSelect = class FieldSelect { | |||
// main table | |||
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) { | |||
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; | |||
// show fields where user has read access and if report hide flag is not set | |||
if (frappe.perm.has_perm(doctype, df.permlevel, "read")) | |||
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) { | |||
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; | |||
// show fields where user has read access and if report hide flag is not set | |||
if (frappe.perm.has_perm(doctype, df.permlevel, "read")) | |||
me.add_field_option(df); | |||
@@ -244,7 +244,7 @@ Object.assign(frappe.utils, { | |||
}; | |||
return String(txt).replace( | |||
/[&<>"'`=/]/g, | |||
/[&<>"'`=/]/g, | |||
char => escape_html_mapping[char] || char | |||
); | |||
}, | |||
@@ -262,7 +262,7 @@ Object.assign(frappe.utils, { | |||
}; | |||
return String(txt).replace( | |||
/&|<|>|"|'|/|`|=/g, | |||
/&|<|>|"|'|/|`|=/g, | |||
char => unescape_html_mapping[char] || char | |||
); | |||
}, | |||
@@ -1435,7 +1435,7 @@ Object.assign(frappe.utils, { | |||
// for link titles | |||
frappe._link_titles = {}; | |||
} | |||
frappe._link_titles[doctype + "::" + name] = value; | |||
}, | |||
@@ -150,18 +150,6 @@ frappe.provide("frappe.views"); | |||
} | |||
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) { | |||
// cache original order | |||
const _cards = this.cards.slice(); | |||
@@ -1343,7 +1343,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { | |||
if (file_format === 'CSV') { | |||
const column_row = this.columns.reduce((acc, col) => { | |||
if (!col.hidden) { | |||
acc.push(col.label); | |||
acc.push(__(col.label)); | |||
} | |||
return acc; | |||
}, []); | |||
@@ -50,7 +50,7 @@ export default class Block { | |||
document.documentElement.addEventListener('mousemove', do_drag, false); | |||
document.documentElement.addEventListener('mouseup', stop_drag, false); | |||
} | |||
function do_drag(e) { | |||
$(this).css("cursor", "col-resize"); | |||
$('.widget').css("pointer-events", "none"); | |||
@@ -72,7 +72,7 @@ export default class Block { | |||
} else { | |||
window.getSelection().removeAllRanges(); | |||
} | |||
} | |||
} | |||
function stop_drag() { | |||
$(this).css("cursor", "default"); | |||
@@ -221,7 +221,7 @@ export default class Block { | |||
$widget_control.prepend($button); | |||
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) { | |||
return; | |||
} | |||
@@ -107,7 +107,7 @@ export default class Header extends Block { | |||
if (data.text !== undefined) { | |||
let text = this._data.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>`; | |||
} | |||
@@ -36,7 +36,7 @@ export default class HeaderSize { | |||
checkState(selection) { | |||
let termWrapper = this.api.selection.findParentTag('SPAN'); | |||
for (const h of ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) { | |||
if (termWrapper && termWrapper.classList.contains(h)) { | |||
let num = h.match(/\d+/)[0]; | |||
@@ -57,7 +57,7 @@ export default class HeaderSize { | |||
span.innerText = range.toString(); | |||
this.remove_parent_tag(range, range.commonAncestorContainer, span); | |||
range.extractContents(); | |||
range.insertNode(span); | |||
this.api.inlineToolbar.close(); | |||
@@ -90,7 +90,7 @@ export default class HeaderSize { | |||
renderActions() { | |||
this.actions = document.createElement('div'); | |||
this.actions.classList = 'header-level-select'; | |||
this.headerLevels = new Array(6).fill().map((_, idx) => { | |||
const $header_level = document.createElement('div'); | |||
$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._element.classList.remove('widget'); | |||
$para_control.appendTo(this.wrapper); | |||
this.wrapper.classList.add('widget', 'paragraph', 'edit-mode'); | |||
this.open_block_list(); | |||
@@ -219,7 +219,7 @@ frappe.views.Workspace = class Workspace { | |||
$sidebar[0].firstElementChild.classList.add("selected"); | |||
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().removeClass('hidden'); | |||
@@ -244,7 +244,7 @@ frappe.views.Workspace = class Workspace { | |||
this.pages[page.name] = data.message; | |||
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 => { | |||
if (settings) { | |||
@@ -596,9 +596,9 @@ frappe.views.Workspace = class Workspace { | |||
} | |||
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]; | |||
let old_item_index = from_pages.findIndex(page => page.title == old_item.title); | |||
duplicate && old_item_index++; | |||
@@ -859,7 +859,7 @@ frappe.views.Workspace = class Workspace { | |||
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) { | |||
$drop_icon.removeClass('hidden'); | |||
} else { | |||
@@ -993,13 +993,13 @@ frappe.views.Workspace = class Workspace { | |||
} | |||
} | |||
}); | |||
this.update_cached_values(new_page, new_page, true, true); | |||
let pre_url = new_page.public ? '' : 'private/'; | |||
let route = pre_url + frappe.router.slug(new_page.title); | |||
frappe.set_route(route); | |||
this.make_sidebar(); | |||
this.show_sidebar_actions(); | |||
}); | |||
@@ -1010,15 +1010,15 @@ frappe.views.Workspace = class Workspace { | |||
validate_page(new_page, old_page) { | |||
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]; | |||
let section = this.sidebar_categories[new_page.is_public]; | |||
if (to_pages && to_pages.filter(p => p.title == new_page.title)[0]) { | |||
message = `Page with title ${new_page.title} already exist.`; | |||
} | |||
} | |||
if (frappe.router.doctype_route_exist(frappe.router.slug(new_page.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 => { | |||
chart_saved_filters = this.update_default_date_filters(filters, chart_saved_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); | |||
}); | |||
} else { | |||
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); | |||
return Promise.resolve(); | |||
} | |||
@@ -993,7 +993,7 @@ jSignatureClass.prototype.resetCanvas = function(data, dontClear){ | |||
ctx.shadowBlur = 0; | |||
} | |||
} | |||
ctx.strokeStyle = settings.color; | |||
// setting up new dataEngine | |||
@@ -12,7 +12,7 @@ | |||
*/ | |||
/* | |||
1. Buttons | |||
*/ | |||
@@ -257,7 +257,7 @@ a.pswp__share--download:hover { | |||
padding: 0 10px; } | |||
/* | |||
4. Caption | |||
*/ | |||
@@ -338,8 +338,8 @@ a.pswp__share--download:hover { | |||
margin: 0; } | |||
.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 | |||
*/ | |||
position: relative; | |||
@@ -409,7 +409,7 @@ a.pswp__share--download:hover { | |||
transform: rotate(0); } } | |||
/* | |||
6. Additional styles | |||
*/ | |||
@@ -5,9 +5,9 @@ | |||
* | |||
* UI on top of main sliding area (caption, arrows, close button, etc.). | |||
* Built just using public methods/properties of PhotoSwipe. | |||
* | |||
* | |||
*/ | |||
(function (root, factory) { | |||
(function (root, factory) { | |||
if (typeof define === 'function' && define.amd) { | |||
define(factory); | |||
} else if (typeof exports === 'object') { | |||
@@ -48,11 +48,11 @@ var PhotoSwipeUI_Default = | |||
_options, | |||
_defaultUIOptions = { | |||
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, | |||
loadingIndicatorDelay: 1000, // 2s | |||
addCaptionHTMLFn: function(item, captionEl /*, isFake */) { | |||
if(!item.title) { | |||
captionEl.children[0].innerHTML = ''; | |||
@@ -92,7 +92,7 @@ var PhotoSwipeUI_Default = | |||
getTextForShare: function( /* shareButtonData */ ) { | |||
return pswp.currItem.title || ''; | |||
}, | |||
indexIndicatorSep: ' / ', | |||
fitControlsWidth: 1200 | |||
@@ -136,12 +136,12 @@ var PhotoSwipeUI_Default = | |||
} | |||
_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. | |||
// | |||
// 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; | |||
_blockControlsTapTimeout = setTimeout(function() { | |||
_blockControlsTap = false; | |||
@@ -172,8 +172,8 @@ var PhotoSwipeUI_Default = | |||
_toggleShareModal = function() { | |||
_shareModalHidden = !_shareModalHidden; | |||
if(!_shareModalHidden) { | |||
_toggleShareModalClass(); | |||
setTimeout(function() { | |||
@@ -189,7 +189,7 @@ var PhotoSwipeUI_Default = | |||
} | |||
}, 300); | |||
} | |||
if(!_shareModalHidden) { | |||
_updateShareURLs(); | |||
} | |||
@@ -211,13 +211,13 @@ var PhotoSwipeUI_Default = | |||
} | |||
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) ); | |||
if(!_shareModalHidden) { | |||
_toggleShareModal(); | |||
} | |||
return false; | |||
}, | |||
_updateShareURLs = function() { | |||
@@ -242,7 +242,7 @@ var PhotoSwipeUI_Default = | |||
shareButtonOut += '<a href="' + shareURL + '" target="_blank" '+ | |||
'class="pswp__share--' + shareButtonData.id + '"' + | |||
(shareButtonData.download ? 'download' : '') + '>' + | |||
(shareButtonData.download ? 'download' : '') + '>' + | |||
shareButtonData.label + '</a>'; | |||
if(_options.parseShareButtonOut) { | |||
@@ -297,7 +297,7 @@ var PhotoSwipeUI_Default = | |||
_setupLoadingIndicator = function() { | |||
// Setup loading indicator | |||
if(_options.preloaderEl) { | |||
_toggleLoadingIndicator(true); | |||
_listen('beforeChange', function() { | |||
@@ -310,18 +310,18 @@ var PhotoSwipeUI_Default = | |||
if(pswp.currItem && pswp.currItem.loading) { | |||
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) | |||
_toggleLoadingIndicator(false); | |||
_toggleLoadingIndicator(false); | |||
// items-controller.js function allowProgressiveImg | |||
} | |||
} else { | |||
_toggleLoadingIndicator(true); // hide preloader | |||
} | |||
}, _options.loadingIndicatorDelay); | |||
}); | |||
_listen('imageLoadComplete', function(index, item) { | |||
if(pswp.currItem === item) { | |||
@@ -341,8 +341,8 @@ var PhotoSwipeUI_Default = | |||
var gap = item.vGap; | |||
if( _fitControlsInViewport() ) { | |||
var bars = _options.barsSize; | |||
var bars = _options.barsSize; | |||
if(_options.captionEl && bars.bottom === 'auto') { | |||
if(!_fakeCaptionContainer) { | |||
_fakeCaptionContainer = framework.createEl('pswp__caption pswp__caption--fake'); | |||
@@ -360,7 +360,7 @@ var PhotoSwipeUI_Default = | |||
} else { | |||
gap.bottom = bars.bottom === 'auto' ? 0 : bars.bottom; | |||
} | |||
// height of top bar is static, no need to calculate it | |||
gap.top = bars.top; | |||
} else { | |||
@@ -371,7 +371,7 @@ var PhotoSwipeUI_Default = | |||
// Hide controls when mouse is used | |||
if(_options.timeToIdle) { | |||
_listen('mouseUsed', function() { | |||
framework.bind(document, 'mousemove', _onIdleMouseMove); | |||
framework.bind(document, 'mouseout', _onMouseLeaveWindow); | |||
@@ -418,77 +418,77 @@ var PhotoSwipeUI_Default = | |||
var _uiElements = [ | |||
{ | |||
name: 'caption', | |||
{ | |||
name: 'caption', | |||
option: 'captionEl', | |||
onInit: function(el) { | |||
_captionContainer = el; | |||
} | |||
onInit: function(el) { | |||
_captionContainer = el; | |||
} | |||
}, | |||
{ | |||
name: 'share-modal', | |||
{ | |||
name: 'share-modal', | |||
option: 'shareEl', | |||
onInit: function(el) { | |||
onInit: function(el) { | |||
_shareModal = el; | |||
}, | |||
onTap: function() { | |||
_toggleShareModal(); | |||
} | |||
} | |||
}, | |||
{ | |||
name: 'button--share', | |||
{ | |||
name: 'button--share', | |||
option: 'shareEl', | |||
onInit: function(el) { | |||
onInit: function(el) { | |||
_shareButton = el; | |||
}, | |||
onTap: function() { | |||
_toggleShareModal(); | |||
} | |||
} | |||
}, | |||
{ | |||
name: 'button--zoom', | |||
{ | |||
name: 'button--zoom', | |||
option: 'zoomEl', | |||
onTap: pswp.toggleDesktopZoom | |||
}, | |||
{ | |||
name: 'counter', | |||
{ | |||
name: 'counter', | |||
option: 'counterEl', | |||
onInit: function(el) { | |||
onInit: function(el) { | |||
_indexIndicator = el; | |||
} | |||
} | |||
}, | |||
{ | |||
name: 'button--close', | |||
{ | |||
name: 'button--close', | |||
option: 'closeEl', | |||
onTap: pswp.close | |||
}, | |||
{ | |||
name: 'button--arrow--left', | |||
{ | |||
name: 'button--arrow--left', | |||
option: 'arrowEl', | |||
onTap: pswp.prev | |||
}, | |||
{ | |||
name: 'button--arrow--right', | |||
{ | |||
name: 'button--arrow--right', | |||
option: 'arrowEl', | |||
onTap: pswp.next | |||
}, | |||
{ | |||
name: 'button--fs', | |||
{ | |||
name: 'button--fs', | |||
option: 'fullscreenEl', | |||
onTap: function() { | |||
onTap: function() { | |||
if(_fullscrenAPI.isFullscreen()) { | |||
_fullscrenAPI.exit(); | |||
} else { | |||
_fullscrenAPI.enter(); | |||
} | |||
} | |||
} | |||
}, | |||
{ | |||
name: 'preloader', | |||
{ | |||
name: 'preloader', | |||
option: 'preloaderEl', | |||
onInit: function(el) { | |||
onInit: function(el) { | |||
_loadingIndicator = el; | |||
} | |||
} | |||
} | |||
]; | |||
@@ -514,12 +514,12 @@ var PhotoSwipeUI_Default = | |||
if(classAttr.indexOf('pswp__' + uiElement.name) > -1 ) { | |||
if( _options[uiElement.option] ) { // if element is not disabled from options | |||
framework.removeClass(item, 'pswp__element--disabled'); | |||
if(uiElement.onInit) { | |||
uiElement.onInit(item); | |||
} | |||
//item.style.display = 'block'; | |||
} else { | |||
framework.addClass(item, 'pswp__element--disabled'); | |||
@@ -538,7 +538,7 @@ var PhotoSwipeUI_Default = | |||
}; | |||
ui.init = function() { | |||
@@ -574,9 +574,9 @@ var PhotoSwipeUI_Default = | |||
_listen('preventDragEvent', function(e, isDown, preventObj) { | |||
var t = e.target || e.srcElement; | |||
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; | |||
} | |||
@@ -634,7 +634,7 @@ var PhotoSwipeUI_Default = | |||
framework.addClass( _controls, 'pswp__ui--hidden'); | |||
ui.setIdle(false); | |||
}); | |||
if(!_options.showAnimationDuration) { | |||
framework.removeClass( _controls, 'pswp__ui--hidden'); | |||
@@ -649,7 +649,7 @@ var PhotoSwipeUI_Default = | |||
}); | |||
_listen('parseVerticalMargin', _applyNavBarGaps); | |||
_setupUIElements(); | |||
if(_options.shareEl && _shareButton && _shareModal) { | |||
@@ -673,7 +673,7 @@ var PhotoSwipeUI_Default = | |||
ui.update = function() { | |||
// Don't update UI if it's hidden | |||
if(_controlsVisible && pswp.currItem) { | |||
ui.updateIndexIndicator(); | |||
if(_options.captionEl) { | |||
@@ -704,19 +704,19 @@ var PhotoSwipeUI_Default = | |||
pswp.setScrollOffset( 0, framework.getScrollY() ); | |||
}, 50); | |||
} | |||
// toogle pswp--fs class on root element | |||
framework[ (_fullscrenAPI.isFullscreen() ? 'add' : 'remove') + 'Class' ](pswp.template, 'pswp--fs'); | |||
}; | |||
ui.updateIndexIndicator = function() { | |||
if(_options.counterEl) { | |||
_indexIndicator.innerHTML = (pswp.getCurrentIndex()+1) + | |||
_options.indexIndicatorSep + | |||
_indexIndicator.innerHTML = (pswp.getCurrentIndex()+1) + | |||
_options.indexIndicatorSep + | |||
_options.getNumItemsFn(); | |||
} | |||
}; | |||
ui.onGlobalTap = function(e) { | |||
e = e || window.event; | |||
var target = e.target || e.srcElement; | |||
@@ -742,7 +742,7 @@ var PhotoSwipeUI_Default = | |||
pswp.toggleDesktopZoom(e.detail.releasePoint); | |||
} | |||
} | |||
} else { | |||
// tap anywhere (except buttons) to toggle visibility of controls | |||
@@ -759,7 +759,7 @@ var PhotoSwipeUI_Default = | |||
pswp.close(); | |||
return; | |||
} | |||
} | |||
}; | |||
ui.onMouseOver = function(e) { | |||
@@ -809,7 +809,7 @@ var PhotoSwipeUI_Default = | |||
eventK: 'moz' + tF | |||
}; | |||
} else if(dE.webkitRequestFullscreen) { | |||
api = { | |||
@@ -829,21 +829,21 @@ var PhotoSwipeUI_Default = | |||
} | |||
if(api) { | |||
api.enter = function() { | |||
api.enter = function() { | |||
// disable close-on-scroll in fullscreen | |||
_initalCloseOnScrollValue = _options.closeOnScroll; | |||
_options.closeOnScroll = false; | |||
_initalCloseOnScrollValue = _options.closeOnScroll; | |||
_options.closeOnScroll = false; | |||
if(this.enterK === 'webkitRequestFullscreen') { | |||
pswp.template[this.enterK]( Element.ALLOW_KEYBOARD_INPUT ); | |||
} else { | |||
return pswp.template[this.enterK](); | |||
return pswp.template[this.enterK](); | |||
} | |||
}; | |||
api.exit = function() { | |||
api.exit = function() { | |||
_options.closeOnScroll = _initalCloseOnScrollValue; | |||
return document[this.exitK](); | |||
return document[this.exitK](); | |||
}; | |||
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]*/, "")), | |||
diff = (((new Date()).getTime() - date.getTime()) / 1000), | |||
day_diff = Math.floor(diff / 86400); | |||
if ( isNaN(day_diff) || day_diff < 0 ) | |||
return ''; | |||
return day_diff == 0 && ( | |||
diff < 60 && "just now" || | |||
diff < 120 && "1 minute ago" || | |||
@@ -12,6 +12,13 @@ | |||
border-bottom: 1px solid var(--table-border-color); | |||
color: var(--text-muted); | |||
font-size: var(--text-md); | |||
.grid-static-col { | |||
.static-area.reqd:after { | |||
content: ' *'; | |||
color: var(--red-400); | |||
} | |||
} | |||
} | |||
.rows .grid-row .data-row, | |||
@@ -1070,11 +1070,11 @@ body { | |||
} | |||
.resizer { | |||
width: 10px; | |||
width: 10px; | |||
height: 100%; | |||
position:absolute; | |||
right: 0; | |||
bottom: 0; | |||
position:absolute; | |||
right: 0; | |||
bottom: 0; | |||
cursor: col-resize; | |||
border-color: transparent; | |||
transition: border-color 0.3s ease-in-out; | |||
@@ -1089,8 +1089,8 @@ body { | |||
margin-bottom: 0 !important; | |||
flex: 1; | |||
&:focus { | |||
outline: none; | |||
&:focus { | |||
outline: none; | |||
} | |||
} | |||
@@ -1124,11 +1124,11 @@ body { | |||
color: var(--text-muted); | |||
border: 1px dashed var(--gray-400); | |||
cursor: pointer; | |||
.widget-control > * { | |||
width: auto; | |||
} | |||
.spacer-left { | |||
min-width: 74px; | |||
} | |||
@@ -1158,7 +1158,7 @@ body { | |||
gap: 5px; | |||
background-color: var(--card-bg); | |||
padding-left: 5px; | |||
.drag-handle { | |||
cursor: all-scroll; | |||
cursor: grabbing; | |||
@@ -1325,7 +1325,7 @@ body { | |||
padding: 6px 10px; | |||
font-size: small; | |||
border-radius: var(--border-radius-sm); | |||
margin: 1px 0px; | |||
margin: 1px 0px; | |||
} | |||
.dropdown-item-icon { | |||
@@ -202,7 +202,7 @@ $level-margin-right: 8px; | |||
box-shadow: none; | |||
margin-left: 0px !important; | |||
border: 1px solid var(--dark-border-color); | |||
&.btn-info { | |||
background-color: var(--gray-400); | |||
border-color: var(--gray-400); | |||
@@ -150,7 +150,7 @@ body { | |||
min-width: 50%; | |||
padding: 0 4px; | |||
margin-bottom: var(--margin-md); | |||
&:last-child { | |||
margin-bottom: 0; | |||
} | |||
@@ -163,18 +163,18 @@ | |||
padding: var(--padding-lg); | |||
box-shadow: var(--card-shadow); | |||
border-radius: var(--border-radius-md); | |||
.new-comment-fields { | |||
flex: 1; | |||
.form-label { | |||
font-weight: var(--text-bold); | |||
} | |||
.comment-text-area textarea { | |||
resize: none; | |||
} | |||
@media (min-width: 576px) { | |||
.comment-by { | |||
padding-right: 0px !important; | |||
@@ -184,7 +184,7 @@ | |||
} | |||
} | |||
} | |||
#comment-list { | |||
position: relative; | |||
@@ -206,7 +206,7 @@ | |||
top: 10px; | |||
left: -17px; | |||
} | |||
.comment-content { | |||
box-shadow: var(--card-shadow); | |||
border-radius: var(--border-radius-md); | |||
@@ -9,7 +9,7 @@ | |||
width: 80% | |||
} | |||
} | |||
.back-to-home { | |||
font-size: var(--text-base); | |||
} | |||
@@ -80,6 +80,8 @@ | |||
.dropdown-menu { | |||
padding: 0.25rem; | |||
box-shadow: var(--shadow-lg); | |||
border-color: var(--gray-200); | |||
} | |||
.dropdown-item { | |||
@@ -308,4 +310,4 @@ h5.modal-title { | |||
.empty-list-icon { | |||
height: 70px; | |||
} | |||
} |
@@ -46,7 +46,7 @@ | |||
.navbar-toggler { | |||
border-color: rgba(255,255,255, 0.1); | |||
.icon { | |||
stroke: none; | |||
} | |||
@@ -1,6 +1,6 @@ | |||
.portal-row { | |||
padding: 1rem 0; | |||
a { | |||
color: $body-color; | |||
} |
@@ -3,7 +3,7 @@ | |||
[data-doctype="Web Form"] { | |||
.page-content-wrapper { | |||
.breadcrumb-container.container { | |||
@include media-breakpoint-up(sm) { | |||
padding-left: 0; | |||
@@ -2,12 +2,14 @@ | |||
# Copyright (c) 2019, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
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 frappe.utils.testutils import add_custom_field, clear_custom_fields | |||
from frappe.desk.form.assign_to import add as assign_to | |||
class TestEnergyPointLog(unittest.TestCase): | |||
class TestEnergyPointLog(FrappeTestCase): | |||
@classmethod | |||
def setUpClass(cls): | |||
settings = frappe.get_single('Energy Point Settings') | |||
@@ -140,9 +142,10 @@ class TestEnergyPointLog(unittest.TestCase): | |||
# for criticism | |||
criticism_points = 2 | |||
todo = create_a_todo(description='Bad patch') | |||
energy_points_before_review = energy_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') | |||
review_points_after_review = get_points('test2@example.com', 'review_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 | |||
}).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({ | |||
'doctype': 'ToDo', | |||
'description': 'Fix a bug', | |||
}).insert() | |||
'description': description, | |||
}).insert(ignore_permissions=True) | |||
def get_points(user, point_type='energy_points'): | |||
@@ -38,6 +38,6 @@ | |||
like | |||
} | |||
}); | |||
} | |||
} | |||
}); | |||
</script> |
@@ -12,7 +12,11 @@ | |||
{# powered #} | |||
<div class="footer-col-right col-sm-6 col-12 footer-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 %} | |||
</div> | |||
</div> | |||
@@ -95,8 +95,6 @@ | |||
min-width: 200px; | |||
padding: 0px; | |||
font-size: 85%; | |||
// only rounded bottoms | |||
border-radius: 0px 0px 4px 4px; | |||
} | |||
@@ -11,7 +11,7 @@ | |||
<p>{{ payment_message or _("Your payment was successfully accepted") }}</p> | |||
{% if not payment_message %} | |||
<div> | |||
<a | |||
<a | |||
href='{{ frappe.form_dict.redirect_to or "/" }}' | |||
class='btn btn-primary btn-sm'> | |||
{{ _("Continue") }} | |||
@@ -97,6 +97,12 @@ class TestReportview(unittest.TestCase): | |||
self.assertFalse(result | |||
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): | |||
data = DatabaseQuery("DocField").execute( | |||
filters={"parent": "DocType"}, fields=["fieldname", "fieldtype"], | |||
@@ -149,7 +155,6 @@ class TestReportview(unittest.TestCase): | |||
filters={"creation": ["between", ["2016-07-06", "2016-07-07"]]}, | |||
fields=["name"]) | |||
def test_ignore_permissions_for_get_filters_cond(self): | |||
frappe.set_user('test2@example.com') | |||
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(frappe.get_all('Nested DocType', {'name': ('not ancestors of', 'Root')})) == len(frappe.get_all('Nested DocType'))) | |||
def test_is_set_is_not_set(self): | |||
res = DatabaseQuery('DocType').execute(filters={'autoname': ['is', 'not set']}) | |||
self.assertTrue({'name': 'Integration Request'} in res) | |||
@@ -319,3 +319,21 @@ class TestDocument(unittest.TestCase): | |||
self.assertIsInstance(doc, Note) | |||
self.assertIsInstance(doc.as_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) | |||
else: | |||
condition = "" | |||
out = frappe.db.multisql({ | |||
'mariadb': """select name from `tab%s` %s | |||
order by RAND() limit 1 offset 0""" % (doctype, condition), | |||
@@ -7,7 +7,7 @@ import re | |||
import frappe | |||
from frappe import _ | |||
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.verified_command import get_signed_params, verify_request | |||
import json | |||
@@ -353,8 +353,8 @@ def process_data_deletion_request(): | |||
for request in requests: | |||
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() | |||
def remove_unverified_record(): | |||
@@ -4,10 +4,10 @@ | |||
import frappe | |||
import unittest | |||
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 ( | |||
create_user_if_not_exists, | |||
create_user_if_not_exists | |||
) | |||
from datetime import datetime, timedelta | |||
@@ -58,3 +58,15 @@ class TestPersonalDataDeletionRequest(unittest.TestCase): | |||
self.assertFalse( | |||
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", | |||
"address", | |||
"footer_items", | |||
"footer_powered", | |||
"footer_template", | |||
"footer_template_values", | |||
"edit_footer_template_values", | |||
@@ -142,7 +143,6 @@ | |||
}, | |||
{ | |||
"collapsible": 1, | |||
"collapsible_depends_on": "top_bar_items", | |||
"fieldname": "top_bar", | |||
"fieldtype": "Section Break", | |||
"label": "Navbar" | |||
@@ -189,7 +189,8 @@ | |||
"description": "Address and other legal information you may want to put in the footer.", | |||
"fieldname": "address", | |||
"fieldtype": "Text Editor", | |||
"label": "Address" | |||
"label": "Address", | |||
"max_height": "8rem" | |||
}, | |||
{ | |||
"fieldname": "footer_items", | |||
@@ -391,6 +392,7 @@ | |||
"label": "App Logo" | |||
}, | |||
{ | |||
"collapsible": 1, | |||
"fieldname": "account_deletion_settings_section", | |||
"fieldtype": "Section Break", | |||
"label": "Account Deletion Settings" | |||
@@ -402,10 +404,15 @@ | |||
"label": "Show Account Deletion Link in My Account Page" | |||
}, | |||
{ | |||
"default": "3", | |||
"default": "72", | |||
"fieldname": "auto_account_deletion", | |||
"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", | |||
@@ -414,7 +421,7 @@ | |||
"issingle": 1, | |||
"links": [], | |||
"max_attachments": 10, | |||
"modified": "2021-12-15 17:28:59.255184", | |||
"modified": "2022-02-24 15:37:22.360138", | |||
"modified_by": "Administrator", | |||
"module": "Website", | |||
"name": "Website Settings", | |||
@@ -437,5 +444,6 @@ | |||
], | |||
"sort_field": "modified", | |||
"sort_order": "ASC", | |||
"states": [], | |||
"track_changes": 1 | |||
} | |||
} |
@@ -120,7 +120,8 @@ def get_website_settings(context=None): | |||
"facebook_share", "google_plus_one", "twitter_share", "linked_in_share", | |||
"disable_signup", "hide_footer_signup", "head_html", "title_prefix", | |||
"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): | |||
context[k] = settings.get(k) | |||
@@ -5,7 +5,7 @@ frappe.ready(function() { | |||
callback: (data) => { | |||
if (data.message) { | |||
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>`; | |||
intro_wrapper.html(intro_wrapper.html() + sla_description_wrapper); | |||
} | |||
@@ -5,6 +5,7 @@ import unittest | |||
from frappe.utils import random_string | |||
from frappe.model.workflow import apply_workflow, WorkflowTransitionError, WorkflowPermissionError, get_common_transition_actions | |||
from frappe.test_runner import make_test_records | |||
from frappe.query_builder import DocType | |||
class TestWorkflow(unittest.TestCase): | |||
@@ -15,9 +16,31 @@ class TestWorkflow(unittest.TestCase): | |||
def setUp(self): | |||
self.workflow = create_todo_workflow() | |||
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): | |||
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): | |||
'''test default condition is set''' | |||
@@ -75,7 +98,7 @@ class TestWorkflow(unittest.TestCase): | |||
actions = get_common_transition_actions([todo1, todo2], 'ToDo') | |||
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") | |||
user = frappe.get_doc('User', 'test2@example.com') | |||
user.add_roles('Test Approver', 'System Manager') | |||
@@ -93,6 +116,32 @@ class TestWorkflow(unittest.TestCase): | |||
self.assertEqual(workflow_actions[0].status, 'Completed') | |||
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): | |||
todo = create_new_todo() | |||
apply_workflow(todo, 'Approve') | |||
@@ -8,9 +8,12 @@ | |||
"status", | |||
"reference_name", | |||
"reference_doctype", | |||
"user", | |||
"workflow_state", | |||
"completed_by" | |||
"column_break_5", | |||
"completed_by_role", | |||
"completed_by", | |||
"permitted_roles", | |||
"user" | |||
], | |||
"fields": [ | |||
{ | |||
@@ -24,16 +27,14 @@ | |||
"fieldname": "reference_name", | |||
"fieldtype": "Dynamic Link", | |||
"label": "Reference Name", | |||
"options": "reference_doctype", | |||
"search_index": 1 | |||
"options": "reference_doctype" | |||
}, | |||
{ | |||
"fieldname": "reference_doctype", | |||
"fieldtype": "Link", | |||
"in_list_view": 1, | |||
"label": "Reference Document Type", | |||
"options": "DocType", | |||
"search_index": 1 | |||
"options": "DocType" | |||
}, | |||
{ | |||
"fieldname": "user", | |||
@@ -47,18 +48,38 @@ | |||
"fieldname": "workflow_state", | |||
"fieldtype": "Data", | |||
"hidden": 1, | |||
"label": "Workflow State", | |||
"search_index": 1 | |||
"label": "Workflow State" | |||
}, | |||
{ | |||
"depends_on": "eval: doc.completed_by", | |||
"fieldname": "completed_by", | |||
"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": [], | |||
"modified": "2021-07-01 09:07:52.848618", | |||
"modified": "2022-02-23 21:06:45.122258", | |||
"modified_by": "Administrator", | |||
"module": "Workflow", | |||
"name": "Workflow Action", | |||
@@ -72,6 +93,7 @@ | |||
], | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"states": [], | |||
"title_field": "reference_name", | |||
"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.utils.user import get_users_with_role | |||
from frappe.utils.data import get_link_to_form | |||
from frappe.query_builder import DocType | |||
class WorkflowAction(Document): | |||
pass | |||
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): | |||
if not user: user = frappe.session.user | |||
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): | |||
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): | |||
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 | |||
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') | |||
next_possible_transitions = get_next_possible_transitions(workflow, get_doc_workflow_state(doc), doc) | |||
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 | |||
create_workflow_actions_for_users(user_data_map.keys(), doc) | |||
create_workflow_actions_for_roles(roles, doc) | |||
if send_email_alert(workflow): | |||
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')) | |||
), 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 | |||
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 | |||
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): | |||
transitions = frappe.get_all('Workflow Transition', | |||
@@ -167,8 +253,10 @@ def get_next_possible_transitions(workflow_name, state, doc=None): | |||
return transitions_to_return | |||
def get_users_next_action_data(transitions, doc): | |||
roles = set() | |||
user_data_map = {} | |||
for transition in transitions: | |||
roles.add(transition.allowed) | |||
users = get_users_with_role(transition.allowed) | |||
filtered_users = filter_allowed_users(users, doc, transition) | |||
for user in filtered_users: | |||
@@ -182,19 +270,24 @@ def get_users_next_action_data(transitions, doc): | |||
'action_name': transition.action, | |||
'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): | |||
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): | |||
return frappe.db.exists({ | |||
'doctype': 'Workflow Action', | |||
'reference_doctype': doc.get('doctype'), | |||
'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): | |||
if not (doctype and name): | |||
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): | |||
workflow_name = get_workflow_name(doc.get('doctype')) | |||
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_bottom_padding=1, | |||
) }} | |||
{% if doc.get({"doctype":"Company History"}) %} | |||
<section class="section section-padding-bottom"> | |||
@@ -87,7 +87,7 @@ | |||
{% if item.target %}target="{{ item.target }}"{% endif %}> | |||
{{ _(item.title or item.label) }} | |||
</a> | |||
{%- endfor %} | |||
{%- endfor %} | |||
</ul> | |||
</div> | |||
</div> |
@@ -10,6 +10,6 @@ no_cache = 1 | |||
def get_context(context): | |||
if frappe.session.user=='Guest': | |||
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.show_sidebar=True |
@@ -53,8 +53,8 @@ | |||
{% endfor %} | |||
{% else %} | |||
<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")}} | |||
</div> | |||
<div class="text-muted mt-2"> | |||