@@ -23,7 +23,7 @@ context('Workspace 2.0', () => { | |||
// check if sidebar item is added in pubic section | |||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0'); | |||
cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click(); | |||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click(); | |||
cy.wait(300); | |||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0'); | |||
@@ -67,7 +67,7 @@ context('Workspace 2.0', () => { | |||
cy.get('.ce-block:last .dropdown-item').contains('Expand').click(); | |||
cy.get(".ce-block:last").should('have.class', 'col-xs-12'); | |||
cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click(); | |||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click(); | |||
}); | |||
it('Delete Private Page', () => { | |||
@@ -80,7 +80,7 @@ context('Workspace 2.0', () => { | |||
.find('.dropdown-item[title="Delete Workspace"]').click({force: true}); | |||
cy.wait(300); | |||
cy.get('.modal-footer > .standard-actions > .btn-modal-primary:visible').first().click(); | |||
cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click(); | |||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click(); | |||
cy.get('.codex-editor__redactor .ce-block'); | |||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('not.exist'); | |||
}); | |||
@@ -358,7 +358,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, | |||
response JSON and shown in a pop-up / modal. | |||
:param msg: Message. | |||
:param title: [optional] Message title. | |||
:param title: [optional] Message title. Default: "Message". | |||
:param raise_exception: [optional] Raise given exception and show message. | |||
:param as_table: [optional] If `msg` is a list of lists, render as HTML table. | |||
:param as_list: [optional] If `msg` is a list, render as un-ordered list. | |||
@@ -395,8 +395,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, | |||
if flags.print_messages and out.message: | |||
print(f"Message: {strip_html_tags(out.message)}") | |||
if title: | |||
out.title = title | |||
out.title = title or _("Message", context="Default title of the message dialog") | |||
if not indicator and raise_exception: | |||
indicator = 'red' | |||
@@ -294,7 +294,6 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No | |||
_sites_path = sites_path | |||
from werkzeug.serving import run_simple | |||
patch_werkzeug_reloader() | |||
if profile or os.environ.get('USE_PROFILER'): | |||
application = ProfilerMiddleware(application, sort_by=('cumtime', 'calls')) | |||
@@ -325,23 +324,3 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No | |||
use_debugger=not in_test_env, | |||
use_evalex=not in_test_env, | |||
threaded=not no_threading) | |||
def patch_werkzeug_reloader(): | |||
""" | |||
This function monkey patches Werkzeug reloader to ignore reloading files in | |||
the __pycache__ directory. | |||
To be deprecated when upgrading to Werkzeug 2. | |||
""" | |||
from werkzeug._reloader import WatchdogReloaderLoop | |||
trigger_reload = WatchdogReloaderLoop.trigger_reload | |||
def custom_trigger_reload(self, filename): | |||
if os.path.basename(os.path.dirname(filename)) == "__pycache__": | |||
return | |||
return trigger_reload(self, filename) | |||
WatchdogReloaderLoop.trigger_reload = custom_trigger_reload |
@@ -70,6 +70,19 @@ class TestComment(unittest.TestCase): | |||
reference_name = test_blog.name | |||
))), 0) | |||
# test for filtering html and css injection elements | |||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) | |||
frappe.form_dict.comment = '<script>alert(1)</script>Comment' | |||
frappe.form_dict.comment_by = 'hacker' | |||
add_comment() | |||
self.assertEqual(frappe.get_all('Comment', fields = ['content'], filters = dict( | |||
reference_doctype = test_blog.doctype, | |||
reference_name = test_blog.name | |||
))[0]['content'], 'Comment') | |||
test_blog.delete() | |||
@@ -2,6 +2,7 @@ | |||
# License: MIT. See LICENSE | |||
from collections import Counter | |||
from typing import List | |||
import frappe | |||
from frappe import _ | |||
from frappe.model.document import Document | |||
@@ -367,15 +368,8 @@ def get_permission_query_conditions_for_communication(user): | |||
return """`tabCommunication`.email_account in ({email_accounts})"""\ | |||
.format(email_accounts=','.join(email_accounts)) | |||
def get_contacts(email_strings, auto_create_contact=False): | |||
email_addrs = [] | |||
for email_string in email_strings: | |||
if email_string: | |||
result = getaddresses([email_string]) | |||
for email in result: | |||
email_addrs.append(email[1]) | |||
def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[str]: | |||
email_addrs = get_emails(email_strings) | |||
contacts = [] | |||
for email in email_addrs: | |||
email = get_email_without_link(email) | |||
@@ -404,6 +398,17 @@ def get_contacts(email_strings, auto_create_contact=False): | |||
return contacts | |||
def get_emails(email_strings: List[str]) -> List[str]: | |||
email_addrs = [] | |||
for email_string in email_strings: | |||
if email_string: | |||
result = getaddresses([email_string]) | |||
for email in result: | |||
email_addrs.append(email[1]) | |||
return email_addrs | |||
def add_contact_links_to_communication(communication, contact_name): | |||
contact_links = frappe.get_all("Dynamic Link", filters={ | |||
"parenttype": "Contact", | |||
@@ -449,8 +454,12 @@ def get_email_without_link(email): | |||
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}): | |||
return email | |||
email_id = email.split("@")[0].split("+")[0] | |||
email_host = email.split("@")[1] | |||
try: | |||
_email = email.split("@") | |||
email_id = _email[0].split("+")[0] | |||
email_host = _email[1] | |||
except IndexError: | |||
return email | |||
return "{0}@{1}".format(email_id, email_host) | |||
@@ -5,6 +5,7 @@ from urllib.parse import quote | |||
import frappe | |||
from frappe.email.doctype.email_queue.email_queue import EmailQueue | |||
from frappe.core.doctype.communication.communication import get_emails | |||
test_records = frappe.get_test_records('Communication') | |||
@@ -201,6 +202,19 @@ class TestCommunication(unittest.TestCase): | |||
self.assertIn(("Note", note.name), doc_links) | |||
def parse_emails(self): | |||
emails = get_emails( | |||
[ | |||
'comm_recipient+DocType+DocName@example.com', | |||
'"First, LastName" <first.lastname@email.com>', | |||
'test@user.com' | |||
] | |||
) | |||
self.assertEqual(emails[0], "comm_recipient+DocType+DocName@example.com") | |||
self.assertEqual(emails[1], "first.lastname@email.com") | |||
self.assertEqual(emails[2], "test@user.com") | |||
class TestCommunicationEmailMixin(unittest.TestCase): | |||
def new_communication(self, recipients=None, cc=None, bcc=None): | |||
recipients = ', '.join(recipients or []) | |||
@@ -781,28 +781,30 @@ def validate_series(dt, autoname=None, name=None): | |||
def validate_links_table_fieldnames(meta): | |||
"""Validate fieldnames in Links table""" | |||
if frappe.flags.in_patch: return | |||
if frappe.flags.in_fixtures: return | |||
if not meta.links: return | |||
if not meta.links or frappe.flags.in_patch or frappe.flags.in_fixtures: | |||
return | |||
for index, link in enumerate(meta.links): | |||
fieldnames = tuple(field.fieldname for field in meta.fields) | |||
for index, link in enumerate(meta.links, 1): | |||
link_meta = frappe.get_meta(link.link_doctype) | |||
if not link_meta.get_field(link.link_fieldname): | |||
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype)) | |||
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype)) | |||
frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname")) | |||
if link.is_child_table and not meta.get_field(link.table_fieldname): | |||
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.table_fieldname), frappe.bold(meta.name)) | |||
frappe.throw(message, frappe.ValidationError, _("Invalid Table Fieldname")) | |||
if not link.is_child_table: | |||
continue | |||
if link.is_child_table: | |||
if not link.parent_doctype: | |||
message = _("Document Links Row #{0}: Parent DocType is mandatory for internal links").format(index+1) | |||
frappe.throw(message, frappe.ValidationError, _("Parent Missing")) | |||
if not link.parent_doctype: | |||
message = _("Document Links Row #{0}: Parent DocType is mandatory for internal links").format(index) | |||
frappe.throw(message, frappe.ValidationError, _("Parent Missing")) | |||
if not link.table_fieldname: | |||
message = _("Document Links Row #{0}: Table Fieldname is mandatory for internal links").format(index+1) | |||
frappe.throw(message, frappe.ValidationError, _("Table Fieldname Missing")) | |||
if not link.table_fieldname: | |||
message = _("Document Links Row #{0}: Table Fieldname is mandatory for internal links").format(index) | |||
frappe.throw(message, frappe.ValidationError, _("Table Fieldname Missing")) | |||
if link.table_fieldname not in fieldnames: | |||
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index, frappe.bold(link.table_fieldname), frappe.bold(meta.name)) | |||
frappe.throw(message, frappe.ValidationError, _("Invalid Table Fieldname")) | |||
def validate_fields_for_doctype(doctype): | |||
meta = frappe.get_meta(doctype, cached=False) | |||
@@ -406,7 +406,7 @@ class TestFile(unittest.TestCase): | |||
test_file.reload() | |||
test_file.file_url = frappe.utils.get_url('unknown.jpg') | |||
test_file.make_thumbnail(suffix="xs") | |||
self.assertEqual(json.loads(frappe.message_log[0]), {"message": f"File '{frappe.utils.get_url('unknown.jpg')}' not found"}) | |||
self.assertEqual(json.loads(frappe.message_log[0]).get("message"), f"File '{frappe.utils.get_url('unknown.jpg')}' not found") | |||
self.assertEquals(test_file.thumbnail_url, None) | |||
def test_file_unzip(self): | |||
@@ -4,7 +4,9 @@ | |||
import frappe, json, os | |||
import unittest | |||
from frappe.desk.query_report import run, save_report | |||
from frappe.desk.reportview import delete_report, save_report as _save_report | |||
from frappe.custom.doctype.customize_form.customize_form import reset_customization | |||
from frappe.core.doctype.user_permission.test_user_permission import create_user | |||
test_records = frappe.get_test_records('Report') | |||
test_dependencies = ['User'] | |||
@@ -30,6 +32,60 @@ class TestReport(unittest.TestCase): | |||
self.assertEqual(columns[1].get('label'), 'Module') | |||
self.assertTrue('User' in [d.get('name') for d in data]) | |||
def test_save_or_delete_report(self): | |||
'''Test for validations when editing / deleting report of type Report Builder''' | |||
try: | |||
report = frappe.get_doc({ | |||
'doctype': 'Report', | |||
'ref_doctype': 'User', | |||
'report_name': 'Test Delete Report', | |||
'report_type': 'Report Builder', | |||
'is_standard': 'No', | |||
}).insert() | |||
# Check for PermissionError | |||
create_user("test_report_owner@example.com", "Website Manager") | |||
frappe.set_user("test_report_owner@example.com") | |||
self.assertRaises(frappe.PermissionError, delete_report, report.name) | |||
# Check for Report Type | |||
frappe.set_user("Administrator") | |||
report.db_set("report_type", "Custom Report") | |||
self.assertRaisesRegex( | |||
frappe.ValidationError, | |||
"Only reports of type Report Builder can be deleted", | |||
delete_report, | |||
report.name | |||
) | |||
# Check if creating and deleting works with proper validations | |||
frappe.set_user("test@example.com") | |||
report_name = _save_report( | |||
'Dummy Report', | |||
'User', | |||
json.dumps([{ | |||
'fieldname': 'email', | |||
'fieldtype': 'Data', | |||
'label': 'Email', | |||
'insert_after_index': 0, | |||
'link_field': 'name', | |||
'doctype': 'User', | |||
'options': 'Email', | |||
'width': 100, | |||
'id':'email', | |||
'name': 'Email' | |||
}]) | |||
) | |||
doc = frappe.get_doc("Report", report_name) | |||
delete_report(doc.name) | |||
finally: | |||
frappe.set_user("Administrator") | |||
frappe.db.rollback() | |||
def test_custom_report(self): | |||
reset_customization('User') | |||
custom_report_name = save_report( | |||
@@ -359,6 +359,7 @@ class TestUser(unittest.TestCase): | |||
json.loads(frappe.message_log[0]).get("message"), | |||
"Password reset instructions have been sent to your email" | |||
) | |||
sendmail.assert_called_once() | |||
self.assertEqual(sendmail.call_args[1]["recipients"], "test2@example.com") | |||
@@ -15,12 +15,12 @@ frappe.ui.form.on('Form Tour', { | |||
frm.add_custom_button(__('Show Tour'), async () => { | |||
const issingle = await check_if_single(frm.doc.reference_doctype); | |||
const name = await get_first_document(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) { | |||
const name = await get_first_document(frm.doc.reference_doctype); | |||
route_changed = frappe.set_route('Form', frm.doc.reference_doctype, name); | |||
} else { | |||
route_changed = frappe.set_route('Form', frm.doc.reference_doctype, 'new'); | |||
@@ -53,73 +53,69 @@ frappe.ui.form.on('Form Tour', { | |||
}; | |||
}); | |||
frm.set_query("field", "steps", function() { | |||
return { | |||
query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list", | |||
filters: { | |||
doctype: frm.doc.reference_doctype, | |||
hidden: 0 | |||
} | |||
}; | |||
}); | |||
frm.set_query("parent_field", "steps", function() { | |||
return { | |||
query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list", | |||
filters: { | |||
doctype: frm.doc.reference_doctype, | |||
fieldtype: "Table", | |||
hidden: 0, | |||
} | |||
}; | |||
}); | |||
frm.trigger('reference_doctype'); | |||
}, | |||
reference_doctype(frm) { | |||
if (!frm.doc.reference_doctype) return; | |||
frappe.db.get_list('DocField', { | |||
filters: { | |||
parent: frm.doc.reference_doctype, | |||
parenttype: 'DocType', | |||
fieldtype: 'Table' | |||
}, | |||
fields: ['options'] | |||
}).then(res => { | |||
if (Array.isArray(res)) { | |||
frm.child_doctypes = res.map(r => r.options); | |||
} | |||
frm.set_fields_as_options( | |||
"fieldname", | |||
frm.doc.reference_doctype, | |||
df => !df.hidden | |||
).then(options => { | |||
frm.fields_dict.steps.grid.update_docfield_property( | |||
"fieldname", | |||
"options", | |||
[""].concat(options) | |||
); | |||
}); | |||
frm.set_fields_as_options( | |||
'parent_fieldname', | |||
frm.doc.reference_doctype, | |||
(df) => df.fieldtype == "Table" && !df.hidden, | |||
).then(options => { | |||
frm.fields_dict.steps.grid.update_docfield_property( | |||
"parent_fieldname", | |||
"options", | |||
[""].concat(options) | |||
); | |||
}); | |||
} | |||
}); | |||
frappe.ui.form.on('Form Tour Step', { | |||
parent_field(frm, cdt, cdn) { | |||
form_render(frm, cdt, cdn) { | |||
if (locals[cdt][cdn].is_table_field) { | |||
frm.trigger('parent_fieldname', cdt, cdn); | |||
} | |||
}, | |||
parent_fieldname(frm, cdt, cdn) { | |||
const child_row = locals[cdt][cdn]; | |||
frappe.model.set_value(cdt, cdn, 'field', ''); | |||
const field_control = get_child_field("steps", cdn, "field"); | |||
field_control.get_query = function() { | |||
return { | |||
query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list", | |||
filters: { | |||
doctype: child_row.child_doctype, | |||
hidden: 0 | |||
} | |||
}; | |||
}; | |||
const parent_fieldname_df = frappe | |||
.get_meta(frm.doc.reference_doctype) | |||
.fields.find(df => df.fieldname == child_row.parent_fieldname); | |||
frm.set_fields_as_options( | |||
'fieldname', | |||
parent_fieldname_df.options, | |||
(df) => !df.hidden, | |||
).then(options => { | |||
frm.fields_dict.steps.grid.update_docfield_property( | |||
"fieldname", | |||
"options", | |||
[""].concat(options) | |||
); | |||
if (child_row.fieldname) { | |||
frappe.model.set_value(cdt, cdn, 'fieldname', child_row.fieldname); | |||
} | |||
}); | |||
} | |||
}); | |||
function get_child_field(child_table, child_name, fieldname) { | |||
// gets the field from grid row form | |||
const grid = cur_frm.fields_dict[child_table].grid; | |||
const grid_row = grid.grid_rows_by_docname[child_name]; | |||
return grid_row.grid_form.fields_dict[fieldname]; | |||
} | |||
async function check_if_single(doctype) { | |||
const { message } = await frappe.db.get_value('DocType', doctype, 'issingle'); | |||
return message.issingle || 0; | |||
@@ -5,58 +5,23 @@ import frappe | |||
from frappe.model.document import Document | |||
from frappe.modules.export_file import export_to_files | |||
class FormTour(Document): | |||
def before_insert(self): | |||
if not self.is_standard: | |||
return | |||
# while syncing, set proper docfield reference | |||
for d in self.steps: | |||
if not frappe.db.exists('DocField', d.field): | |||
d.field = frappe.db.get_value('DocField', { | |||
'fieldname': d.fieldname, 'parent': self.reference_doctype, 'fieldtype': d.fieldtype | |||
}, "name") | |||
if d.is_table_field and not frappe.db.exists('DocField', d.parent_field): | |||
d.parent_field = frappe.db.get_value('DocField', { | |||
'fieldname': d.parent_fieldname, 'parent': self.reference_doctype, 'fieldtype': 'Table' | |||
}, "name") | |||
class FormTour(Document): | |||
def before_save(self): | |||
meta = frappe.get_meta(self.reference_doctype) | |||
for step in self.steps: | |||
if step.is_table_field and step.parent_fieldname: | |||
parent_field_df = meta.get_field(step.parent_fieldname) | |||
step.child_doctype = parent_field_df.options | |||
field_df = frappe.get_meta(step.child_doctype).get_field(step.fieldname) | |||
step.label = field_df.label | |||
step.fieldtype = field_df.fieldtype | |||
else: | |||
field_df = meta.get_field(step.fieldname) | |||
step.label = field_df.label | |||
step.fieldtype = field_df.fieldtype | |||
def on_update(self): | |||
if frappe.conf.developer_mode and self.is_standard: | |||
export_to_files([['Form Tour', self.name]], self.module) | |||
def before_export(self, doc): | |||
for d in doc.steps: | |||
d.field = "" | |||
d.parent_field = "" | |||
@frappe.whitelist() | |||
@frappe.validate_and_sanitize_search_inputs | |||
def get_docfield_list(doctype, txt, searchfield, start, page_len, filters): | |||
or_filters = [ | |||
['fieldname', 'like', '%' + txt + '%'], | |||
['label', 'like', '%' + txt + '%'], | |||
['fieldtype', 'like', '%' + txt + '%'] | |||
] | |||
parent_doctype = filters.get('doctype') | |||
fieldtype = filters.get('fieldtype') | |||
if not fieldtype: | |||
excluded_fieldtypes = ['Column Break'] | |||
excluded_fieldtypes += filters.get('excluded_fieldtypes', []) | |||
fieldtype_filter = ['not in', excluded_fieldtypes] | |||
else: | |||
fieldtype_filter = fieldtype | |||
docfields = frappe.get_all( | |||
doctype, | |||
fields=["name as value", "label", "fieldtype"], | |||
filters={'parent': parent_doctype, 'fieldtype': fieldtype_filter}, | |||
or_filters=or_filters, | |||
limit_start=start, | |||
limit_page_length=page_len, | |||
order_by="idx", | |||
as_list=1, | |||
) | |||
return docfields | |||
export_to_files([["Form Tour", self.name]], self.module) |
@@ -6,19 +6,17 @@ | |||
"field_order": [ | |||
"is_table_field", | |||
"section_break_2", | |||
"parent_field", | |||
"field", | |||
"parent_fieldname", | |||
"fieldname", | |||
"title", | |||
"description", | |||
"column_break_2", | |||
"position", | |||
"label", | |||
"fieldtype", | |||
"has_next_condition", | |||
"next_step_condition", | |||
"section_break_13", | |||
"fieldname", | |||
"parent_fieldname", | |||
"fieldtype", | |||
"child_doctype" | |||
], | |||
"fields": [ | |||
@@ -38,23 +36,13 @@ | |||
"reqd": 1 | |||
}, | |||
{ | |||
"depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_field))", | |||
"fieldname": "field", | |||
"fieldtype": "Link", | |||
"label": "Field", | |||
"options": "DocField", | |||
"reqd": 1 | |||
}, | |||
{ | |||
"fetch_from": "field.fieldname", | |||
"depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_fieldname))", | |||
"fieldname": "fieldname", | |||
"fieldtype": "Data", | |||
"hidden": 1, | |||
"fieldtype": "Select", | |||
"label": "Fieldname", | |||
"read_only": 1 | |||
"reqd": 1 | |||
}, | |||
{ | |||
"fetch_from": "field.label", | |||
"fieldname": "label", | |||
"fieldtype": "Data", | |||
"in_list_view": 1, | |||
@@ -88,10 +76,8 @@ | |||
}, | |||
{ | |||
"default": "0", | |||
"fetch_from": "field.fieldtype", | |||
"fieldname": "fieldtype", | |||
"fieldtype": "Data", | |||
"hidden": 1, | |||
"label": "Fieldtype", | |||
"read_only": 1 | |||
}, | |||
@@ -105,14 +91,6 @@ | |||
"fieldname": "section_break_2", | |||
"fieldtype": "Section Break" | |||
}, | |||
{ | |||
"depends_on": "is_table_field", | |||
"fieldname": "parent_field", | |||
"fieldtype": "Link", | |||
"label": "Parent Field", | |||
"mandatory_depends_on": "is_table_field", | |||
"options": "DocField" | |||
}, | |||
{ | |||
"fieldname": "section_break_13", | |||
"fieldtype": "Section Break", | |||
@@ -120,7 +98,6 @@ | |||
"label": "Hidden Fields" | |||
}, | |||
{ | |||
"fetch_from": "parent_field.options", | |||
"fieldname": "child_doctype", | |||
"fieldtype": "Data", | |||
"hidden": 1, | |||
@@ -128,18 +105,17 @@ | |||
"read_only": 1 | |||
}, | |||
{ | |||
"fetch_from": "parent_field.fieldname", | |||
"depends_on": "is_table_field", | |||
"fieldname": "parent_fieldname", | |||
"fieldtype": "Data", | |||
"hidden": 1, | |||
"label": "Parent Fieldname", | |||
"read_only": 1 | |||
"fieldtype": "Select", | |||
"label": "Parent Field", | |||
"mandatory_depends_on": "is_table_field" | |||
} | |||
], | |||
"index_web_pages_for_search": 1, | |||
"istable": 1, | |||
"links": [], | |||
"modified": "2021-06-06 20:52:21.076972", | |||
"modified": "2022-01-27 15:18:36.481801", | |||
"modified_by": "Administrator", | |||
"module": "Desk", | |||
"name": "Form Tour Step", | |||
@@ -147,5 +123,6 @@ | |||
"permissions": [], | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"states": [], | |||
"track_changes": 1 | |||
} |
@@ -20,13 +20,13 @@ | |||
"hide_custom", | |||
"public", | |||
"content", | |||
"section_break_2", | |||
"tab_break_2", | |||
"charts", | |||
"section_break_15", | |||
"tab_break_15", | |||
"shortcuts", | |||
"section_break_18", | |||
"tab_break_18", | |||
"links", | |||
"roles_section", | |||
"roles_tab", | |||
"roles" | |||
], | |||
"fields": [ | |||
@@ -40,8 +40,8 @@ | |||
{ | |||
"collapsible": 1, | |||
"collapsible_depends_on": "charts", | |||
"fieldname": "section_break_2", | |||
"fieldtype": "Section Break", | |||
"fieldname": "tab_break_2", | |||
"fieldtype": "Tab Break", | |||
"label": "Dashboards" | |||
}, | |||
{ | |||
@@ -78,15 +78,15 @@ | |||
{ | |||
"collapsible": 1, | |||
"collapsible_depends_on": "shortcuts", | |||
"fieldname": "section_break_15", | |||
"fieldtype": "Section Break", | |||
"fieldname": "tab_break_15", | |||
"fieldtype": "Tab Break", | |||
"label": "Shortcuts" | |||
}, | |||
{ | |||
"collapsible": 1, | |||
"collapsible_depends_on": "links", | |||
"fieldname": "section_break_18", | |||
"fieldtype": "Section Break", | |||
"fieldname": "tab_break_18", | |||
"fieldtype": "Tab Break", | |||
"label": "Link Cards" | |||
}, | |||
{ | |||
@@ -152,14 +152,14 @@ | |||
"options": "Has Role" | |||
}, | |||
{ | |||
"fieldname": "roles_section", | |||
"fieldtype": "Section Break", | |||
"fieldname": "roles_tab", | |||
"fieldtype": "Tab Break", | |||
"label": "Roles" | |||
} | |||
], | |||
"in_create": 1, | |||
"links": [], | |||
"modified": "2021-12-15 19:33:00.805265", | |||
"modified": "2022-01-27 12:06:13.111743", | |||
"modified_by": "Administrator", | |||
"module": "Desk", | |||
"name": "Workspace", | |||
@@ -6,7 +6,6 @@ from frappe.utils import strip, cint | |||
from frappe.translate import (set_default_language, get_dict, send_translations) | |||
from frappe.geo.country_info import get_country_info | |||
from frappe.utils.password import update_password | |||
from werkzeug.useragents import UserAgent | |||
from . import install_fixtures | |||
def get_setup_stages(args): | |||
@@ -315,17 +314,10 @@ def prettify_args(args): | |||
return pretty_args | |||
def email_setup_wizard_exception(traceback, args): | |||
if not frappe.local.conf.setup_wizard_exception_email: | |||
if not frappe.conf.setup_wizard_exception_email: | |||
return | |||
pretty_args = prettify_args(args) | |||
if frappe.local.request: | |||
user_agent = UserAgent(frappe.local.request.headers.get('User-Agent', '')) | |||
else: | |||
user_agent = frappe._dict() | |||
message = """ | |||
#### Traceback | |||
@@ -349,18 +341,15 @@ def email_setup_wizard_exception(traceback, args): | |||
#### Basic Information | |||
- **Site:** {site} | |||
- **User:** {user} | |||
- **Browser:** {user_agent.platform} {user_agent.browser} version: {user_agent.version} language: {user_agent.language} | |||
- **Browser Languages**: `{accept_languages}`""".format( | |||
- **User:** {user}""".format( | |||
site=frappe.local.site, | |||
traceback=traceback, | |||
args="\n".join(pretty_args), | |||
user=frappe.session.user, | |||
user_agent=user_agent, | |||
headers=frappe.local.request.headers, | |||
accept_languages=", ".join(frappe.local.request.accept_languages.values())) | |||
headers=frappe.request.headers, | |||
) | |||
frappe.sendmail(recipients=frappe.local.conf.setup_wizard_exception_email, | |||
frappe.sendmail(recipients=frappe.conf.setup_wizard_exception_email, | |||
sender=frappe.session.user, | |||
subject="Setup failed: {}".format(frappe.local.site), | |||
message=message, | |||
@@ -262,22 +262,66 @@ def compress(data, args=None): | |||
} | |||
@frappe.whitelist() | |||
def save_report(): | |||
"""save report""" | |||
data = frappe.local.form_dict | |||
if frappe.db.exists('Report', data['name']): | |||
d = frappe.get_doc('Report', data['name']) | |||
def save_report(name, doctype, report_settings): | |||
"""Save reports of type Report Builder from Report View""" | |||
if frappe.db.exists('Report', name): | |||
report = frappe.get_doc('Report', name) | |||
if report.is_standard == "Yes": | |||
frappe.throw(_("Standard Reports cannot be edited")) | |||
if report.report_type != "Report Builder": | |||
frappe.throw(_("Only reports of type Report Builder can be edited")) | |||
if ( | |||
report.owner != frappe.session.user | |||
and not frappe.has_permission("Report", "write") | |||
): | |||
frappe.throw( | |||
_("Insufficient Permissions for editing Report"), | |||
frappe.PermissionError | |||
) | |||
else: | |||
d = frappe.new_doc('Report') | |||
d.report_name = data['name'] | |||
d.ref_doctype = data['doctype'] | |||
d.report_type = "Report Builder" | |||
d.json = data['json'] | |||
frappe.get_doc(d).save() | |||
frappe.msgprint(_("{0} is saved").format(d.name), alert=True) | |||
return d.name | |||
report = frappe.new_doc('Report') | |||
report.report_name = name | |||
report.ref_doctype = doctype | |||
report.report_type = "Report Builder" | |||
report.json = report_settings | |||
report.save(ignore_permissions=True) | |||
frappe.msgprint( | |||
_("Report {0} saved").format(frappe.bold(report.name)), | |||
indicator="green", | |||
alert=True, | |||
) | |||
return report.name | |||
@frappe.whitelist() | |||
def delete_report(name): | |||
"""Delete reports of type Report Builder from Report View""" | |||
report = frappe.get_doc("Report", name) | |||
if report.is_standard == "Yes": | |||
frappe.throw(_("Standard Reports cannot be deleted")) | |||
if report.report_type != "Report Builder": | |||
frappe.throw(_("Only reports of type Report Builder can be deleted")) | |||
if ( | |||
report.owner != frappe.session.user | |||
and not frappe.has_permission("Report", "delete") | |||
): | |||
frappe.throw( | |||
_("Insufficient Permissions for deleting Report"), | |||
frappe.PermissionError | |||
) | |||
report.delete(ignore_permissions=True) | |||
frappe.msgprint( | |||
_("Report {0} deleted").format(frappe.bold(report.name)), | |||
indicator="green", | |||
alert=True, | |||
) | |||
@frappe.whitelist() | |||
@frappe.read_only() | |||
@@ -9,7 +9,7 @@ import frappe | |||
from frappe import _, msgprint, is_whitelisted | |||
from frappe.utils import flt, cstr, now, get_datetime_str, file_lock, date_diff | |||
from frappe.model.base_document import BaseDocument, get_controller | |||
from frappe.model.naming import set_new_name | |||
from frappe.model.naming import set_new_name, validate_name | |||
from frappe.model.docstatus import DocStatus | |||
from frappe.model import optional_fields, table_fields | |||
from frappe.model.workflow import validate_workflow | |||
@@ -416,12 +416,12 @@ class Document(BaseDocument): | |||
# If autoname has set as Prompt (name) | |||
if self.get("__newname"): | |||
self.name = self.get("__newname") | |||
self.name = validate_name(self.doctype, self.get("__newname")) | |||
self.flags.name_set = True | |||
return | |||
if set_name: | |||
self.name = set_name | |||
self.name = validate_name(self.doctype, set_name) | |||
else: | |||
set_new_name(self) | |||
@@ -549,14 +549,14 @@ frappe.ui.form.Dashboard = class FormDashboard { | |||
render_graph(args) { | |||
this.chart_area.show(); | |||
this.chart_area.body.empty(); | |||
$.extend({ | |||
$.extend(args, { | |||
type: 'line', | |||
colors: ['green'], | |||
truncateLegends: 1, | |||
axisOptions: { | |||
shortenYAxisNumbers: 1 | |||
} | |||
}, args); | |||
}); | |||
this.show(); | |||
this.chart = new frappe.Chart('.form-graph', args); | |||
@@ -151,7 +151,7 @@ frappe.ui.form.FormTour = class FormTour { | |||
const curr_step = step_info; | |||
const next_step = this.tour.steps[curr_step.idx]; | |||
const is_next_field_in_curr_table = next_step.parent_field == curr_step.field; | |||
const is_next_field_in_curr_table = next_step.parent_fieldname == curr_step.fieldname; | |||
if (!is_next_field_in_curr_table) return; | |||
@@ -233,7 +233,7 @@ frappe.msgprint = function(msg, title, is_minimizable) { | |||
if(data.title || !msg_exists) { | |||
// set title only if it is explicitly given | |||
// and no existing title exists | |||
frappe.msg_dialog.set_title(data.title || __('Message')); | |||
frappe.msg_dialog.set_title(data.title || __('Message', null, 'Default title of the message dialog')); | |||
} | |||
// show / hide indicator | |||
@@ -283,12 +283,13 @@ class NotificationsView extends BaseNotificationsView { | |||
e.stopImmediatePropagation(); | |||
this.mark_as_read(field.name, item_html); | |||
}); | |||
item_html.on('click', () => { | |||
this.mark_as_read(field.name, item_html); | |||
}); | |||
} | |||
item_html.on('click', () => { | |||
!field.read && this.mark_as_read(field.name, item_html); | |||
this.notifications_icon.trigger('click'); | |||
}); | |||
return item_html; | |||
} | |||
@@ -18,7 +18,6 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { | |||
setup_defaults() { | |||
super.setup_defaults(); | |||
this.page_title = __('Report:') + ' ' + this.page_title; | |||
this.menu_items = this.report_menu_items(); | |||
this.view = 'Report'; | |||
const route = frappe.get_route(); | |||
@@ -52,6 +51,11 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { | |||
this.page.main.addClass('report-view'); | |||
} | |||
setup_page() { | |||
this.menu_items = this.report_menu_items(); | |||
super.setup_page(); | |||
} | |||
toggle_side_bar() { | |||
super.toggle_side_bar(); | |||
// refresh datatable when sidebar is toggled to accomodate extra space | |||
@@ -1207,7 +1211,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { | |||
args: { | |||
name: name, | |||
doctype: this.doctype, | |||
json: JSON.stringify(report_settings) | |||
report_settings: JSON.stringify(report_settings) | |||
}, | |||
callback:(r) => { | |||
if(r.exc) { | |||
@@ -1244,6 +1248,17 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { | |||
} | |||
} | |||
delete_report() { | |||
return frappe.call({ | |||
method: 'frappe.desk.reportview.delete_report', | |||
args: { name: this.report_name }, | |||
callback(response) { | |||
if (response.exc) return; | |||
window.history.back(); | |||
} | |||
}); | |||
} | |||
get_column_widths() { | |||
if (this.datatable) { | |||
return this.datatable | |||
@@ -1465,12 +1480,42 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { | |||
} | |||
}); | |||
// save buttons | |||
if(frappe.user.is_report_manager()) { | |||
items = items.concat([ | |||
{ label: __('Save'), action: () => this.save_report('save') }, | |||
{ label: __('Save As'), action: () => this.save_report('save_as') } | |||
]); | |||
const can_edit_or_delete = (action) => { | |||
const method = action == "delete" ? "can_delete" : "can_write"; | |||
return ( | |||
this.report_doc | |||
&& this.report_doc.is_standard !== "Yes" | |||
&& ( | |||
frappe.model[method]("Report") | |||
|| this.report_doc.owner === frappe.session.user | |||
) | |||
); | |||
}; | |||
// A user with role Report Manager or Report Owner can save | |||
if (can_edit_or_delete()) { | |||
items.push({ | |||
label: __("Save"), | |||
action: () => this.save_report('save') | |||
}); | |||
} | |||
// anyone can save as | |||
items.push({ | |||
label: __('Save As'), | |||
action: () => this.save_report('save_as') | |||
}); | |||
// A user with role Report Manager or Report Owner can delete | |||
if (can_edit_or_delete("delete")) { | |||
items.push({ | |||
label: __("Delete"), | |||
action: () => frappe.confirm( | |||
"Are you sure you want to delete this report?", | |||
() => this.delete_report(), | |||
), | |||
shortcut: "Shift+Ctrl+D" | |||
}); | |||
} | |||
// user permissions | |||
@@ -62,7 +62,7 @@ export default class Paragraph extends Block { | |||
this.show_hide_block_list(); | |||
}); | |||
div.addEventListener('blur', () => { | |||
setTimeout(() => this.show_hide_block_list(true), 10); | |||
!this.over_block_list_item && this.show_hide_block_list(true); | |||
}); | |||
div.dataset.placeholder = this.api.i18n.t(this._placeholder); | |||
div.addEventListener('keyup', this.onKeyUp); | |||
@@ -95,6 +95,12 @@ export default class Paragraph extends Block { | |||
this.api.caret.setToBlock(index); | |||
}); | |||
$block_list_item.mouseenter(() => { | |||
this.over_block_list_item = true; | |||
}).mouseleave(() => { | |||
this.over_block_list_item = false; | |||
}); | |||
$block_list_container.append($block_list_item); | |||
}); | |||
@@ -376,7 +376,7 @@ frappe.views.Workspace = class Workspace { | |||
this.clear_page_actions(); | |||
page.is_editable && this.page.set_primary_action( | |||
__("Save Customizations"), | |||
__("Save"), | |||
() => { | |||
this.clear_page_actions(); | |||
this.save_page(page).then((saved) => { | |||
@@ -1158,7 +1158,7 @@ frappe.views.Workspace = class Workspace { | |||
item.data.card_name !== 'Custom Reports') | |||
); | |||
if (page.content == JSON.stringify(blocks)) { | |||
if (page.content == JSON.stringify(blocks) && Object.keys(new_widgets).length === 0) { | |||
this.setup_customization_buttons(page); | |||
frappe.show_alert({ message: __("No changes made on the page"), indicator: "warning" }); | |||
return false; | |||
@@ -100,7 +100,7 @@ export default class Widget { | |||
let title = max_chars ? frappe.ellipsis(base, max_chars) : base; | |||
if (this.icon) { | |||
let icon = frappe.utils.icon(this.icon); | |||
let icon = frappe.utils.icon(this.icon, "lg"); | |||
this.title_field[0].innerHTML = `${icon} <span class="ellipsis" title="${title}">${title}</span>`; | |||
} else { | |||
this.title_field[0].innerHTML = `<span class="ellipsis" title="${title}">${title}</span>`; | |||
@@ -154,7 +154,7 @@ class CardDialog extends WidgetDialog { | |||
{ | |||
fieldtype: "Data", | |||
fieldname: "label", | |||
label: "Label", | |||
label: "Label" | |||
}, | |||
{ | |||
fieldname: 'links', | |||
@@ -174,7 +174,7 @@ class CardDialog extends WidgetDialog { | |||
}, | |||
{ | |||
fieldname: "icon", | |||
fieldtype: "Data", | |||
fieldtype: "Icon", | |||
label: "Icon" | |||
}, | |||
{ | |||
@@ -182,6 +182,7 @@ class CardDialog extends WidgetDialog { | |||
fieldtype: "Select", | |||
in_list_view: 1, | |||
label: "Link Type", | |||
reqd: 1, | |||
options: ["DocType", "Page", "Report"] | |||
}, | |||
{ | |||
@@ -189,9 +190,9 @@ class CardDialog extends WidgetDialog { | |||
fieldtype: "Dynamic Link", | |||
in_list_view: 1, | |||
label: "Link To", | |||
reqd: 1, | |||
get_options: (df) => { | |||
return df.doc.link_type; | |||
} | |||
}, | |||
{ | |||
@@ -227,6 +228,31 @@ class CardDialog extends WidgetDialog { | |||
} | |||
process_data(data) { | |||
data.links.map((item, idx) => { | |||
let message = ''; | |||
let row = idx+1; | |||
if (!item.link_type) { | |||
message = "Following fields have missing values: <br><br><ul>"; | |||
message += `<li>Link Type in Row ${row}</li>`; | |||
} | |||
if (!item.link_to) { | |||
message += `<li>Link To in Row ${row}</li>`; | |||
} | |||
if (message) { | |||
message += "</ul>"; | |||
frappe.throw({ | |||
message: __(message), | |||
title: __("Missing Values Required"), | |||
indicator: 'orange' | |||
}); | |||
} | |||
item.label = item.label ? item.label : item.link_to; | |||
}); | |||
data.label = data.label ? data.label : data.chart_name; | |||
return data; | |||
} | |||
@@ -155,6 +155,7 @@ body { | |||
svg { | |||
flex: none; | |||
margin-right: 6px; | |||
margin-left: -2px; | |||
box-shadow: none; | |||
} | |||
} | |||
@@ -560,21 +561,29 @@ body { | |||
} | |||
&.links-widget-box { | |||
padding: 18px 12px; | |||
.link-item { | |||
display: flex; | |||
text-decoration: none; | |||
font-size: var(--text-md); | |||
color: var(--text-color); | |||
padding: var(--padding-xs); | |||
margin-left: -5px; | |||
padding: 4px; | |||
margin-left: -4px; | |||
margin-bottom: 4px; | |||
border-radius: var(--border-radius-md); | |||
cursor: pointer; | |||
&:hover { | |||
background-color: var(--bg-color); | |||
background-color: var(--fg-hover-color); | |||
.indicator-pill { | |||
background-color: var(--fg-color); | |||
} | |||
} | |||
&:first-child { | |||
margin-top: 15px; | |||
margin-top: 18px; | |||
} | |||
&:last-child { | |||
@@ -601,6 +610,8 @@ body { | |||
.indicator-pill { | |||
margin-right: var(--margin-sm); | |||
height: 20px; | |||
padding: 3px 8px; | |||
} | |||
} | |||
} | |||
@@ -850,10 +861,16 @@ body { | |||
} | |||
} | |||
.layout-main-section-wrapper { | |||
margin-top: -5px; | |||
padding-top: 5px; | |||
} | |||
.layout-main-section { | |||
background-color: var(--fg-color); | |||
padding: var(--padding-sm); | |||
box-shadow: var(--card-shadow); | |||
border-radius: var(--border-radius-lg); | |||
padding: var(--padding-sm); | |||
} | |||
.block-menu-item-icon svg{ | |||
@@ -6,6 +6,7 @@ from frappe.website.utils import clear_cache | |||
from frappe.rate_limiter import rate_limit | |||
from frappe.utils import add_to_date, now | |||
from frappe.website.doctype.blog_settings.blog_settings import get_comment_limit | |||
from frappe.utils.html_utils import clean_html | |||
from frappe import _ | |||
@@ -29,7 +30,7 @@ def add_comment(comment, comment_email, comment_by, reference_doctype, reference | |||
return False | |||
comment = doc.add_comment( | |||
text=comment, | |||
text=clean_html(comment), | |||
comment_email=comment_email, | |||
comment_by=comment_by) | |||
@@ -10,11 +10,11 @@ from frappe.model.naming import append_number_if_name_exists, revert_series_if_l | |||
from frappe.model.naming import determine_consecutive_week_number, parse_naming_series | |||
class TestNaming(unittest.TestCase): | |||
def setUp(self): | |||
frappe.db.delete('Note') | |||
def tearDown(self): | |||
# Reset ToDo autoname to hash | |||
todo_doctype = frappe.get_doc('DocType', 'ToDo') | |||
todo_doctype.autoname = 'hash' | |||
todo_doctype.save() | |||
frappe.db.rollback() | |||
def test_append_number_if_name_exists(self): | |||
''' | |||
@@ -203,4 +203,51 @@ class TestNaming(unittest.TestCase): | |||
dt = datetime.fromisoformat("2021-12-31") | |||
w = determine_consecutive_week_number(dt) | |||
self.assertEqual(w, "52") | |||
self.assertEqual(w, "52") | |||
def test_naming_validations(self): | |||
# case 1: check same name as doctype | |||
# set name via prompt | |||
tag = frappe.get_doc({ | |||
'doctype': 'Tag', | |||
'__newname': 'Tag' | |||
}) | |||
self.assertRaises(frappe.NameError, tag.insert) | |||
# set by passing set_name as ToDo | |||
self.assertRaises(frappe.NameError, make_invalid_todo) | |||
# set new name - Note | |||
note = frappe.get_doc({ | |||
'doctype': 'Note', | |||
'title': 'Note' | |||
}) | |||
self.assertRaises(frappe.NameError, note.insert) | |||
# case 2: set name with "New ---" | |||
tag = frappe.get_doc({ | |||
'doctype': 'Tag', | |||
'__newname': 'New Tag' | |||
}) | |||
self.assertRaises(frappe.NameError, tag.insert) | |||
# case 3: set name with special characters | |||
tag = frappe.get_doc({ | |||
'doctype': 'Tag', | |||
'__newname': 'Tag<>' | |||
}) | |||
self.assertRaises(frappe.NameError, tag.insert) | |||
# case 4: no name specified | |||
tag = frappe.get_doc({ | |||
'doctype': 'Tag', | |||
'__newname': '' | |||
}) | |||
self.assertRaises(frappe.ValidationError, tag.insert) | |||
def make_invalid_todo(): | |||
frappe.get_doc({ | |||
'doctype': 'ToDo', | |||
'description': 'Test' | |||
}).insert(set_name='ToDo') |
@@ -148,9 +148,6 @@ def create_form_tour(): | |||
if frappe.db.exists('Form Tour', {'name': 'Test Form Tour'}): | |||
return | |||
def get_docfield_name(filters): | |||
return frappe.db.get_value('DocField', filters, "name") | |||
tour = frappe.get_doc({ | |||
'doctype': 'Form Tour', | |||
'title': 'Test Form Tour', | |||
@@ -161,7 +158,6 @@ def create_form_tour(): | |||
"description": "Test Description 1", | |||
"has_next_condition": 1, | |||
"next_step_condition": "eval: doc.first_name", | |||
"field": get_docfield_name({'parent': 'Contact', 'fieldname': 'first_name'}), | |||
"fieldname": "first_name", | |||
"fieldtype": "Data" | |||
},{ | |||
@@ -169,21 +165,18 @@ def create_form_tour(): | |||
"description": "Test Description 2", | |||
"has_next_condition": 1, | |||
"next_step_condition": "eval: doc.last_name", | |||
"field": get_docfield_name({'parent': 'Contact', 'fieldname': 'last_name'}), | |||
"fieldname": "last_name", | |||
"fieldtype": "Data" | |||
},{ | |||
"title": "Test Title 3", | |||
"description": "Test Description 3", | |||
"field": get_docfield_name({'parent': 'Contact', 'fieldname': 'phone_nos'}), | |||
"fieldname": "phone_nos", | |||
"fieldtype": "Table" | |||
},{ | |||
"title": "Test Title 4", | |||
"description": "Test Description 4", | |||
"is_table_field": 1, | |||
"parent_field": get_docfield_name({'parent': 'Contact', 'fieldname': 'phone_nos'}), | |||
"field": get_docfield_name({'parent': 'Contact Phone', 'fieldname': 'phone'}), | |||
"parent_fieldname": "phone_nos", | |||
"next_step_condition": "eval: doc.phone", | |||
"has_next_condition": 1, | |||
"fieldname": "phone", | |||
@@ -47,7 +47,7 @@ | |||
"localforage": "^1.9.0", | |||
"moment": "^2.20.1", | |||
"moment-timezone": "^0.5.28", | |||
"node-sass": "^4.14.1", | |||
"node-sass": "^7.0.0", | |||
"plyr": "^3.6.2", | |||
"popper.js": "^1.16.0", | |||
"quagga": "^0.12.1", | |||
@@ -21,7 +21,7 @@ googlemaps~=4.4.5 | |||
gunicorn~=20.1.0 | |||
html2text==2020.1.16 | |||
html5lib~=1.1 | |||
ipython~=7.27.0 | |||
ipython~=7.31.1 | |||
Jinja2~=3.0.1 | |||
ldap3~=2.9 | |||
markdown2~=2.4.0 | |||
@@ -32,7 +32,7 @@ openpyxl~=3.0.7 | |||
passlib~=1.7.4 | |||
paytmchecksum~=1.7.0 | |||
pdfkit~=0.6.1 | |||
Pillow~=8.2.0 | |||
Pillow~=9.0.0 | |||
premailer~=3.8.0 | |||
psutil~=5.8.0 | |||
psycopg2-binary~=2.9.1 | |||
@@ -63,7 +63,7 @@ sqlparse~=0.4.1 | |||
stripe~=2.56.0 | |||
terminaltables~=3.1.0 | |||
urllib3~=1.26.4 | |||
Werkzeug~=0.16.1 | |||
Werkzeug~=2.0.3 | |||
Whoosh~=2.7.4 | |||
wrapt~=1.12.1 | |||
xlrd~=2.0.1 | |||