Merge branch 'develop' of https://github.com/frappe/frappe into link_title_refactor

This commit is contained in:
Saqib Ansari 2022-01-14 15:20:56 +05:30
commit 30ba577c0a
66 changed files with 1059 additions and 449 deletions

View file

@ -12,7 +12,7 @@ jobs:
- name: 'Setup Environment'
uses: actions/setup-python@v2
with:
python-version: 3.7
python-version: 3.8
- name: 'Clone repo'
uses: actions/checkout@v2

View file

@ -12,6 +12,8 @@ Read the documentation: https://frappeframework.com/docs
"""
import os, warnings
STANDARD_USERS = ('Guest', 'Administrator')
_dev_server = os.environ.get('DEV_SERVER', False)
if _dev_server:
@ -121,6 +123,7 @@ def set_user_lang(user, user_language=None):
local.lang = get_user_lang(user)
# local-globals
db = local("db")
qb = local("qb")
conf = local("conf")

View file

@ -250,8 +250,7 @@ class LoginManager:
if not self.user:
return
from frappe.core.doctype.user.user import STANDARD_USERS
if self.user in STANDARD_USERS:
if self.user in frappe.STANDARD_USERS:
return False
reset_pwd_after_days = cint(frappe.db.get_single_value("System Settings",

View file

@ -17,7 +17,7 @@ from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_p
from frappe.model.base_document import get_controller
from frappe.social.doctype.post.post import frequently_visited_links
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo
from frappe.utils import get_time_zone
from frappe.utils import get_time_zone, add_user_info
def get_bootinfo():
"""build and return boot info"""
@ -223,17 +223,14 @@ def load_translations(bootinfo):
bootinfo["__messages"] = messages
def get_user_info():
user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image', 'gender',
'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type', 'time_zone'],
filters=dict(enabled=1))
# get info for current user
user_info = frappe._dict()
add_user_info(frappe.session.user, user_info)
user_info_map = {d.name: d for d in user_info}
if frappe.session.user == 'Administrator' and user_info.Administrator.email:
user_info[user_info.Administrator.email] = user_info.Administrator
admin_data = user_info_map.get('Administrator')
if admin_data:
user_info_map[admin_data.email] = admin_data
return user_info_map
return user_info
def get_user(bootinfo):
"""get user info"""

View file

@ -12,7 +12,7 @@ from frappe.core.utils import get_parent_doc
from frappe.utils.bot import BotReply
from frappe.utils import parse_addr, split_emails
from frappe.core.doctype.comment.comment import update_comment_in_doc
from email.utils import parseaddr
from email.utils import getaddresses
from urllib.parse import unquote
from frappe.utils.user import is_system_user
from frappe.contacts.doctype.contact.contact import get_contact_name
@ -372,10 +372,9 @@ def get_contacts(email_strings, auto_create_contact=False):
for email_string in email_strings:
if email_string:
for email in email_string.split(","):
parsed_email = parseaddr(email)[1]
if parsed_email:
email_addrs.append(parsed_email)
result = getaddresses([email_string])
for email in result:
email_addrs.append(email[1])
contacts = []
for email in email_addrs:

View file

@ -314,7 +314,7 @@ class DataExporter:
.where(child_doctype_table.parentfield == c["parentfield"])
.orderby(child_doctype_table.idx)
)
for ci, child in enumerate(data_row.run()):
for ci, child in enumerate(data_row.run(as_dict=True)):
self.add_data_row(rows, c['doctype'], c['parentfield'], child, ci)
for row in rows:

View file

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE
import unittest
import frappe
from frappe.core.doctype.data_export.exporter import DataExporter
class TestDataExporter(unittest.TestCase):
def setUp(self):
self.doctype_name = 'Test DocType for Export Tool'
self.doc_name = 'Test Data for Export Tool'
self.create_doctype_if_not_exists(doctype_name=self.doctype_name)
self.create_test_data()
def create_doctype_if_not_exists(self, doctype_name, force=False):
"""
Helper Function for setting up doctypes
"""
if force:
frappe.delete_doc_if_exists('DocType', doctype_name)
frappe.delete_doc_if_exists('DocType', 'Child 1 of ' + doctype_name)
if frappe.db.exists('DocType', doctype_name):
return
# Child Table 1
table_1_name = 'Child 1 of ' + doctype_name
frappe.get_doc({
'doctype': 'DocType',
'name': table_1_name,
'module': 'Custom',
'custom': 1,
'istable': 1,
'fields': [
{'label': 'Child Title', 'fieldname': 'child_title', 'reqd': 1, 'fieldtype': 'Data'},
{'label': 'Child Number', 'fieldname': 'child_number', 'fieldtype': 'Int'},
]
}).insert()
# Main Table
frappe.get_doc({
'doctype': 'DocType',
'name': doctype_name,
'module': 'Custom',
'custom': 1,
'autoname': 'field:title',
'fields': [
{'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'},
{'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'},
{'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name},
],
'permissions': [
{'role': 'System Manager'}
]
}).insert()
def create_test_data(self, force=False):
"""
Helper Function creating test data
"""
if force:
frappe.delete_doc(self.doctype_name, self.doc_name)
if not frappe.db.exists(self.doctype_name, self.doc_name):
self.doc = frappe.get_doc(
doctype=self.doctype_name,
title=self.doc_name,
number="100",
table_field_1=[
{"child_title": "Child Title 1", "child_number": "50"},
{"child_title": "Child Title 2", "child_number": "51"},
]
).insert()
else:
self.doc = frappe.get_doc(self.doctype_name, self.doc_name)
def test_export_content(self):
exp = DataExporter(doctype=self.doctype_name, file_type='CSV')
exp.build_response()
self.assertEqual(frappe.response['type'],'csv')
self.assertEqual(frappe.response['doctype'], self.doctype_name)
self.assertTrue(frappe.response['result'])
self.assertIn('Child Title 1\",50',frappe.response['result'])
self.assertIn('Child Title 2\",51',frappe.response['result'])
def test_export_type(self):
for type in ['csv', 'Excel']:
with self.subTest(type=type):
exp = DataExporter(doctype=self.doctype_name, file_type=type)
exp.build_response()
self.assertEqual(frappe.response['doctype'], self.doctype_name)
self.assertTrue(frappe.response['result'])
if type == 'csv':
self.assertEqual(frappe.response['type'],'csv')
elif type == 'Excel':
self.assertEqual(frappe.response['type'],'binary')
self.assertEqual(frappe.response['filename'], self.doctype_name+'.xlsx') # 'Test DocType for Export Tool.xlsx')
self.assertTrue(frappe.response['filecontent'])
def tearDown(self):
pass

View file

@ -39,7 +39,8 @@ def sync_languages():
frappe.get_doc({
'doctype': 'Language',
'language_code': l['code'],
'language_name': l['name']
'language_name': l['name'],
'enabled': 1,
}).insert()
def update_language_names():

View file

@ -12,6 +12,7 @@
"restrict_to_domain",
"column_break_4",
"disabled",
"is_custom",
"desk_access",
"two_factor_auth",
"navigation_settings_section",
@ -24,8 +25,7 @@
"form_settings_section",
"form_sidebar",
"timeline",
"dashboard",
"is_custom"
"dashboard"
],
"fields": [
{
@ -148,7 +148,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-10-08 14:06:55.729364",
"modified": "2022-01-12 20:18:18.496230",
"modified_by": "Administrator",
"module": "Core",
"name": "Role",
@ -170,5 +170,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View file

@ -19,7 +19,7 @@ from frappe.core.doctype.user_type.user_type import user_linked_with_permission_
from frappe.query_builder import DocType
STANDARD_USERS = ("Guest", "Administrator")
STANDARD_USERS = frappe.STANDARD_USERS
class User(Document):
__new_password = None

View file

@ -37,16 +37,14 @@ class UserType(Document):
return
modules = frappe.get_all("DocType",
fields=["module"],
filters={"name": ("in", [d.document_type for d in self.user_doctypes])},
distinct=True,
pluck="module",
)
self.set('user_type_modules', [])
for row in modules:
self.append('user_type_modules', {
'module': row.module
})
self.set("user_type_modules", [])
for module in modules:
self.append("user_type_modules", {"module": module})
def validate_document_type_limit(self):
limit = frappe.conf.get('user_type_doctype_limit', {}).get(frappe.scrub(self.name))

View file

@ -74,9 +74,15 @@ class PostgresDatabase(Database):
return conn
def escape(self, s, percent=True):
"""Excape quotes and percent in given string."""
"""Escape quotes and percent in given string."""
if isinstance(s, bytes):
s = s.decode('utf-8')
# MariaDB's driver treats None as an empty string
# So Postgres should do the same
if s is None:
s = ''
if percent:
s = s.replace("%", "%%")

View file

@ -7,6 +7,7 @@ from frappe.model.document import Document
from frappe import _
from frappe.utils import cint
class BulkUpdate(Document):
pass
@ -22,7 +23,7 @@ def update(doctype, field, value, condition='', limit=500):
frappe.throw(_('; not allowed in condition'))
docnames = frappe.db.sql_list(
'''select name from `tab{0}`{1} limit 0, {2}'''.format(doctype, condition, limit)
'''select name from `tab{0}`{1} limit {2} offset 0'''.format(doctype, condition, limit)
)
data = {}
data[field] = value

View file

@ -1,23 +1,33 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# Copyright (c) 2022, Frappe Technologies and contributors
# License: MIT. See LICENSE
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
from frappe.config import get_modules_from_all_apps_for_user
import json
import frappe
from frappe import _
import json
from frappe.config import get_modules_from_all_apps_for_user
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
from frappe.query_builder import DocType
class Dashboard(Document):
def on_update(self):
if self.is_default:
# make all other dashboards non-default
frappe.db.sql('''update
tabDashboard set is_default = 0 where name != %s''', self.name)
DashBoard = DocType("Dashboard")
frappe.qb.update(DashBoard).set(
DashBoard.is_default, 0
).where(
DashBoard.name != self.name
).run()
if frappe.conf.developer_mode and self.is_standard:
export_to_files(record_list=[['Dashboard', self.name, self.module + ' Dashboard']], record_module=self.module)
export_to_files(
record_list=[["Dashboard", self.name, f"{self.module} Dashboard"]],
record_module=self.module
)
def validate(self):
if not frappe.conf.developer_mode and self.is_standard:

View file

@ -94,30 +94,78 @@ def get_docinfo(doc=None, doctype=None, name=None):
automated_messages = filter(lambda x: x['communication_type'] == 'Automated Message', all_communications)
communications_except_auto_messages = filter(lambda x: x['communication_type'] != 'Automated Message', all_communications)
frappe.response["docinfo"] = {
docinfo = frappe._dict(user_info = {})
add_comments(doc, docinfo)
docinfo.update({
"attachments": get_attachments(doc.doctype, doc.name),
"attachment_logs": get_comments(doc.doctype, doc.name, 'attachment'),
"communications": communications_except_auto_messages,
"automated_messages": automated_messages,
'comments': get_comments(doc.doctype, doc.name),
'total_comments': len(json.loads(doc.get('_comments') or '[]')),
'versions': get_versions(doc),
"assignments": get_assignments(doc.doctype, doc.name),
"assignment_logs": get_comments(doc.doctype, doc.name, 'assignment'),
"permissions": get_doc_permissions(doc),
"shared": frappe.share.get_users(doc.doctype, doc.name),
"info_logs": get_comments(doc.doctype, doc.name, comment_type=['Info', 'Edit', 'Label']),
"share_logs": get_comments(doc.doctype, doc.name, 'share'),
"like_logs": get_comments(doc.doctype, doc.name, 'Like'),
"workflow_logs": get_comments(doc.doctype, doc.name, comment_type="Workflow"),
"views": get_view_logs(doc.doctype, doc.name),
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
"additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name),
"milestones": get_milestones(doc.doctype, doc.name),
"is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user),
"tags": get_tags(doc.doctype, doc.name),
"document_email": get_document_email(doc.doctype, doc.name)
}
"document_email": get_document_email(doc.doctype, doc.name),
})
update_user_info(docinfo)
frappe.response["docinfo"] = docinfo
def add_comments(doc, docinfo):
# divide comments into separate lists
docinfo.comments = []
docinfo.shared = []
docinfo.assignment_logs = []
docinfo.attachment_logs = []
docinfo.info_logs = []
docinfo.like_logs = []
docinfo.workflow_logs = []
comments = frappe.get_all("Comment",
fields=["name", "creation", "content", "owner", "comment_type"],
filters={
"reference_doctype": doc.doctype,
"reference_name": doc.name
}
)
for c in comments:
if c.comment_type == "Comment":
c.content = frappe.utils.markdown(c.content)
docinfo.comments.append(c)
elif c.comment_type in ('Shared', 'Unshared'):
docinfo.shared.append(c)
elif c.comment_type in ('Assignment Completed', 'Assigned'):
docinfo.assignment_logs.append(c)
elif c.comment_type in ('Attachment', 'Attachment Removed'):
docinfo.attachment_logs.append(c)
elif c.comment_type in ('Info', 'Edit', 'Label'):
docinfo.info_logs.append(c)
elif c.comment_type == "Like":
docinfo.like_logs.append(c)
elif c.comment_type == "Workflow":
docinfo.workflow_logs.append(c)
frappe.utils.add_user_info(c.owner, docinfo.user_info)
return comments
def get_milestones(doctype, name):
return frappe.db.get_all('Milestone', fields = ['creation', 'owner', 'track_field', 'value'],
@ -252,7 +300,7 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=
return communications
def get_assignments(dt, dn):
cl = frappe.get_all("ToDo",
return frappe.get_all("ToDo",
fields=['name', 'allocated_to as owner', 'description', 'status'],
filters={
'reference_type': dt,
@ -260,8 +308,6 @@ def get_assignments(dt, dn):
'status': ('!=', 'Cancelled'),
})
return cl
@frappe.whitelist()
def get_badge_info(doctypes, filters):
filters = json.loads(filters)
@ -371,3 +417,25 @@ def send_link_titles(link_titles):
frappe.local.response["_link_titles"] = {}
frappe.local.response["_link_titles"].update(link_titles)
def update_user_info(docinfo):
for d in docinfo.communications:
frappe.utils.add_user_info(d.sender, docinfo.user_info)
for d in docinfo.shared:
frappe.utils.add_user_info(d.user, docinfo.user_info)
for d in docinfo.assignments:
frappe.utils.add_user_info(d.owner, docinfo.user_info)
for d in docinfo.views:
frappe.utils.add_user_info(d.owner, docinfo.user_info)
@frappe.whitelist()
def get_user_info_for_viewers(users):
user_info = {}
for user in json.loads(users):
frappe.utils.add_user_info(user, user_info)
return user_info

View file

@ -388,7 +388,6 @@ def make_records(records, debug=False):
# LOG every success and failure
for record in records:
doctype = record.get("doctype")
condition = record.get('__condition')
@ -405,6 +404,7 @@ def make_records(records, debug=False):
try:
doc.insert(ignore_permissions=True)
frappe.db.commit()
except frappe.DuplicateEntryError as e:
# print("Failed to insert duplicate {0} {1}".format(doctype, doc.name))
@ -417,6 +417,7 @@ def make_records(records, debug=False):
raise
except Exception as e:
frappe.db.rollback()
exception = record.get('__exception')
if exception:
config = _dict(exception)

View file

@ -12,7 +12,7 @@ from io import StringIO
from frappe.core.doctype.access_log.access_log import make_access_log
from frappe.utils import cstr, format_duration
from frappe.model.base_document import get_controller
from frappe.utils import add_user_info
@frappe.whitelist()
@frappe.read_only()
@ -219,6 +219,8 @@ def compress(data, args=None):
"""separate keys and values"""
from frappe.desk.query_report import add_total_row
user_info = {}
if not data: return data
if args is None:
args = {}
@ -230,13 +232,19 @@ def compress(data, args=None):
new_row.append(row.get(key))
values.append(new_row)
# add user info for assignments (avatar)
if row._assign:
for user in json.loads(row._assign):
add_user_info(user, user_info)
if args.get("add_total_row"):
meta = frappe.get_meta(args.doctype)
values = add_total_row(values, keys, meta)
return {
"keys": keys,
"values": values
"values": values,
"user_info": user_info
}
@frappe.whitelist()

View file

@ -4,6 +4,7 @@
import frappe
from frappe import _
@frappe.whitelist()
def get_all_nodes(doctype, label, parent, tree_method, **filters):
'''Recursively gets all data from tree nodes'''
@ -40,8 +41,8 @@ def get_children(doctype, parent='', **filters):
def _get_children(doctype, parent='', ignore_permissions=False):
parent_field = 'parent_' + doctype.lower().replace(' ', '_')
filters = [['ifnull(`{0}`,"")'.format(parent_field), '=', parent],
['docstatus', '<' ,'2']]
filters = [["ifnull(`{0}`,'')".format(parent_field), '=', parent],
['docstatus', '<' ,2]]
meta = frappe.get_meta(doctype)

View file

@ -475,28 +475,20 @@ class QueueBuilder:
if self._unsubscribed_user_emails is not None:
return self._unsubscribed_user_emails
all_ids = tuple(set(self.recipients + self.cc))
all_ids = list(set(self.recipients + self.cc))
unsubscribed = frappe.db.sql_list('''
SELECT
distinct email
from
`tabEmail Unsubscribe`
where
email in %(all_ids)s
and (
(
reference_doctype = %(reference_doctype)s
and reference_name = %(reference_name)s
)
or global_unsubscribe = 1
)
''', {
'all_ids': all_ids,
'reference_doctype': self.reference_doctype,
'reference_name': self.reference_name,
})
EmailUnsubscribe = frappe.qb.DocType("Email Unsubscribe")
unsubscribed = (frappe.qb.from_(EmailUnsubscribe)
.select(EmailUnsubscribe.email)
.where(EmailUnsubscribe.email.isin(all_ids) &
(
(
(EmailUnsubscribe.reference_doctype == self.reference_doctype) & (EmailUnsubscribe.reference_name == self.reference_name)
) | EmailUnsubscribe.global_unsubscribe == 1
)
).distinct()
).run(pluck=True)
self._unsubscribed_user_emails = unsubscribed or []
return self._unsubscribed_user_emails

View file

@ -27,11 +27,7 @@ from frappe.utils.html_utils import clean_email_html
# fix due to a python bug in poplib that limits it to 2048
poplib._MAXLINE = 20480
imaplib._MAXLINE = 20480
# fix due to a python bug in poplib that limits it to 2048
poplib._MAXLINE = 20480
imaplib._MAXLINE = 20480
class EmailSizeExceededError(frappe.ValidationError): pass

View file

@ -96,7 +96,7 @@
},
{
"fieldname": "authorization_uri",
"fieldtype": "Data",
"fieldtype": "Small Text",
"label": "Authorization URI",
"mandatory_depends_on": "eval:doc.redirect_uri"
},
@ -139,7 +139,7 @@
"link_fieldname": "connected_app"
}
],
"modified": "2021-05-10 05:03:06.296863",
"modified": "2022-01-07 05:28:45.073041",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Connected App",

View file

@ -646,8 +646,6 @@ class BaseDocument(object):
value, comma_options))
def _validate_data_fields(self):
from frappe.core.doctype.user.user import STANDARD_USERS
# data_field options defined in frappe.model.data_field_options
for data_field in self.meta.get_data_fields():
data = self.get(data_field.fieldname)
@ -658,7 +656,7 @@ class BaseDocument(object):
continue
if data_field_options == "Email":
if (self.owner in STANDARD_USERS) and (data in STANDARD_USERS):
if (self.owner in frappe.STANDARD_USERS) and (data in frappe.STANDARD_USERS):
continue
for email_address in frappe.utils.split_emails(data):
frappe.utils.validate_email_address(email_address, throw=True)
@ -768,7 +766,9 @@ class BaseDocument(object):
else:
self_value = self.get_value(key)
# Postgres stores values as `datetime.time`, MariaDB as `timedelta`
if isinstance(self_value, datetime.timedelta) and isinstance(db_value, datetime.time):
db_value = datetime.timedelta(hours=db_value.hour, minutes=db_value.minute, seconds=db_value.second, microseconds=db_value.microsecond)
if self_value != db_value:
frappe.throw(_("Not allowed to change {0} after submission").format(df.label),
frappe.UpdateAfterSubmitError)
@ -1008,15 +1008,12 @@ def _filter(data, filters, limit=None):
_filters[f] = fval
for d in data:
add = True
for f, fval in _filters.items():
if not frappe.compare(getattr(d, f, None), fval[0], fval[1]):
add = False
break
if add:
else:
out.append(d)
if limit and (len(out)-1)==limit:
if limit and len(out) >= limit:
break
return out

View file

@ -130,6 +130,11 @@ class DatabaseQuery(object):
args.fields = 'distinct ' + args.fields
args.order_by = '' # TODO: recheck for alternative
# Postgres requires any field that appears in the select clause to also
# appear in the order by and group by clause
if frappe.db.db_type == 'postgres' and args.order_by and args.group_by:
args = self.prepare_select_args(args)
query = """select %(fields)s
from %(tables)s
%(conditions)s
@ -203,6 +208,19 @@ class DatabaseQuery(object):
return args
def prepare_select_args(self, args):
order_field = re.sub(r"\ order\ by\ |\ asc|\ ASC|\ desc|\ DESC", "", args.order_by)
if order_field not in args.fields:
extracted_column = order_column = order_field.replace("`", "")
if "." in extracted_column:
extracted_column = extracted_column.split(".")[1]
args.fields += f", MAX({extracted_column}) as `{order_column}`"
args.order_by = args.order_by.replace(order_field, f"`{order_column}`")
return args
def parse_args(self):
"""Convert fields and filters from strings to list, dicts"""
if isinstance(self.fields, str):

View file

@ -0,0 +1,6 @@
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.4844 25.3281V16.0781" stroke="#F56B6B" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M42.6719 25.3281V16.0781" stroke="#F56B6B" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M34.5781 50.7656C30.8982 50.7656 27.3691 49.3038 24.767 46.7017C22.165 44.0997 20.7031 40.5705 20.7031 36.8906V25.3281H48.4531V36.8906C48.4531 40.5705 46.9913 44.0997 44.3892 46.7017C41.7872 49.3038 38.258 50.7656 34.5781 50.7656Z" stroke="#98A1A9" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M57.7032 58.8594C63.3462 53.4851 66.9411 46.3131 67.8703 38.576C68.7994 30.8388 67.0046 23.0197 62.7944 16.4622C58.5842 9.90464 52.2215 5.01829 44.7997 2.64279C37.3778 0.267296 29.3604 0.550997 22.125 3.44515C14.8896 6.33929 8.8882 11.6632 5.15204 18.5018C1.41588 25.3405 0.178293 33.267 1.65196 40.9191C3.12562 48.5713 7.21851 55.4712 13.2273 60.4332C19.236 65.3952 26.7855 68.1094 34.5782 68.1094V56.5469" stroke="#98A1A9" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -148,8 +148,9 @@ frappe.ui.form.Control = class BaseControl {
return this.doc[this.df.fieldname];
}
}
set_value(value) {
return this.validate_and_set_in_model(value);
set_value(value, force_set_value=false) {
return this.validate_and_set_in_model(value, null, force_set_value);
}
parse_validate_and_set_in_model(value, e) {
if(this.parse) {
@ -157,12 +158,11 @@ frappe.ui.form.Control = class BaseControl {
}
return this.validate_and_set_in_model(value, e);
}
validate_and_set_in_model(value, e) {
var me = this;
let force_value_set = (this.doc && this.doc.__run_link_triggers);
let is_value_same = (this.get_model_value() === value);
validate_and_set_in_model(value, e, force_set_value=false) {
const me = this;
const is_value_same = (this.get_model_value() === value);
if (this.inside_change_event || (!force_value_set && is_value_same)) {
if (this.inside_change_event || (is_value_same && !force_set_value)) {
return Promise.resolve();
}

View file

@ -10,14 +10,16 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
this.set_t_for_today();
}
set_formatted_input(value) {
if (value === "Today") {
value = this.get_now_date();
}
super.set_formatted_input(value);
if (this.timepicker_only) return;
if (!this.datepicker) return;
if (!value) {
this.datepicker.clear();
return;
} else if (value === "Today") {
value = this.get_now_date();
}
let should_refresh = this.last_value && this.last_value !== value;
@ -78,7 +80,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
}
get_start_date() {
return new Date(this.get_now_date());
return this.get_now_date();
}
set_datepicker() {
@ -117,7 +119,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
this.datepicker.update('position', position);
}
get_now_date() {
return frappe.datetime.convert_to_system_tz(frappe.datetime.now_date(true));
return frappe.datetime.convert_to_system_tz(frappe.datetime.now_date(true), false).toDate();
}
set_t_for_today() {
var me = this;

View file

@ -32,7 +32,9 @@ frappe.ui.form.ControlMarkdownEditor = class ControlMarkdownEditor extends frapp
}
set_language() {
this.df.options = 'Markdown';
if (!this.df.options) {
this.df.options = 'Markdown';
}
super.set_language();
}

View file

@ -983,7 +983,7 @@ frappe.ui.form.Form = class FrappeForm {
$.each(this.fields_dict, function(fieldname, field) {
if (field.df.fieldtype=="Link" && this.doc[fieldname]) {
// triggers add fetch, sets value in model and runs triggers
field.set_value(this.doc[fieldname]);
field.set_value(this.doc[fieldname], true);
}
});

View file

@ -27,19 +27,40 @@ frappe.ui.form.FormViewers.set_users = function(data, type) {
const users = data.users || [];
const new_users = users.filter(user => !past_users.includes(user));
frappe.model.set_docinfo(doctype, docname, type, {
past: past_users.concat(new_users),
new: new_users,
current: users
});
if (new_users.length===0) return;
if (
cur_frm &&
cur_frm.doc &&
cur_frm.doc.doctype === doctype &&
cur_frm.doc.name == docname &&
cur_frm.viewers
) {
cur_frm.viewers.refresh(true, type);
const set_and_refresh = () => {
const info = {
past: past_users.concat(new_users),
new: new_users,
current: users
};
frappe.model.set_docinfo(doctype, docname, type, info);
if (
cur_frm &&
cur_frm.doc &&
cur_frm.doc.doctype === doctype &&
cur_frm.doc.name == docname &&
cur_frm.viewers
) {
cur_frm.viewers.refresh(true, type);
}
};
let unknown_users = [];
for (let user of users) {
if (!frappe.boot.user_info[user]) unknown_users.push(user);
}
if (unknown_users.length===0) {
set_and_refresh();
} else {
// load additional user info
frappe.xcall('frappe.desk.form.load.get_user_info_for_viewers', {users: unknown_users}).then((data) => {
Object.assign(frappe.boot.user_info, data);
set_and_refresh();
});
}
};

View file

@ -484,6 +484,11 @@ frappe.views.BaseList = class BaseList {
prepare_data(r) {
let data = r.message || {};
// extract user_info for assignments
Object.assign(frappe.boot.user_info, data.user_info);
delete data.user_info;
data = !Array.isArray(data)
? frappe.utils.dict(data.keys, data.values)
: data;

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
$.extend(frappe.model, {
Object.assign(frappe.model, {
docinfo: {},
sync: function(r) {
/* docs:
@ -33,22 +33,28 @@ $.extend(frappe.model, {
}
if(d.localname) {
frappe.model.new_names[d.localname] = d.name;
$(document).trigger('rename', [d.doctype, d.localname, d.name]);
delete locals[d.doctype][d.localname];
// update docinfo to new dict keys
if(i===0) {
frappe.model.docinfo[d.doctype][d.name] = frappe.model.docinfo[d.doctype][d.localname];
frappe.model.docinfo[d.doctype][d.localname] = undefined;
}
frappe.model.rename_after_save(d, i);
}
}
}
frappe.model.sync_docinfo(r);
},
rename_after_save: (d, i) => {
frappe.model.new_names[d.localname] = d.name;
$(document).trigger('rename', [d.doctype, d.localname, d.name]);
delete locals[d.doctype][d.localname];
// update docinfo to new dict keys
if(i===0) {
frappe.model.docinfo[d.doctype][d.name] = frappe.model.docinfo[d.doctype][d.localname];
frappe.model.docinfo[d.doctype][d.localname] = undefined;
}
},
sync_docinfo: (r) => {
// set docinfo (comments, assign, attachments)
if(r.docinfo) {
var doc;
@ -62,10 +68,14 @@ $.extend(frappe.model, {
frappe.model.docinfo[doc.doctype] = {};
frappe.model.docinfo[doc.doctype][doc.name] = r.docinfo;
}
// copy values to frappe.boot.user_info
Object.assign(frappe.boot.user_info, r.docinfo.user_info);
}
return r.docs;
},
add_to_locals: function(doc) {
if(!locals[doc.doctype])
locals[doc.doctype] = {};
@ -100,6 +110,7 @@ $.extend(frappe.model, {
}
}
},
update_in_locals: function(doc) {
// update values in the existing local doc instead of replacing
let local_doc = locals[doc.doctype][doc.name];

View file

@ -296,11 +296,18 @@ frappe.request.call = function(opts) {
})
.fail(function(xhr, textStatus) {
try {
if (xhr.responseText) {
var data = JSON.parse(xhr.responseText);
if (data.exception) {
// frappe.exceptions.CustomError -> CustomError
var exception = data.exception.split('.').at(-1);
if (xhr.getResponseHeader('content-type') == 'application/json' && xhr.responseText) {
var data;
try {
data = JSON.parse(xhr.responseText);
} catch (e) {
console.log("Unable to parse reponse text");
console.log(xhr.responseText);
console.log(e);
}
if (data && data.exception) {
// frappe.exceptions.CustomError: (1024, ...) -> CustomError
var exception = data.exception.split('.').at(-1).split(':').at(0);
var exception_handler = exception_handlers[exception];
if (exception_handler) {
exception_handler(data);

View file

@ -2,14 +2,6 @@ frappe.user_info = function(uid) {
if(!uid)
uid = frappe.session.user;
if(uid.toLowerCase()==="bot") {
return {
fullname: __("Bot"),
image: "/assets/frappe/images/ui/bot.png",
abbr: "B"
};
}
if(!(frappe.boot.user_info && frappe.boot.user_info[uid])) {
var user_info = {fullname: uid || "Unknown"};
} else {
@ -22,29 +14,6 @@ frappe.user_info = function(uid) {
return user_info;
};
frappe.ui.set_user_background = function(src, selector, style) {
if(!selector) selector = "#page-desktop";
if(!style) style = "Fill Screen";
if(src) {
if (window.cordova && src.indexOf("http") === -1) {
src = frappe.base_url + src;
}
var background = repl('background: url("%(src)s") center center;', {src: src});
} else {
var background = "background-color: #4B4C9D;";
}
frappe.dom.set_style(repl('%(selector)s { \
%(background)s \
background-attachment: fixed; \
%(style)s \
}', {
selector:selector,
background:background,
style: style==="Fill Screen" ? "background-size: cover;" : ""
}));
};
frappe.provide('frappe.user');
$.extend(frappe.user, {

View file

@ -139,8 +139,6 @@ export default class WebFormList {
make_table_head() {
// Create Heading
let thead = this.table.createTHead();
thead.style.backgroundColor = "#f7fafc";
thead.style.color = "#8d99a6";
let row = thead.insertRow();
let th = document.createElement("th");

View file

@ -165,6 +165,16 @@
--bg-pink: var(--pink-50);
--bg-cyan: var(--cyan-50);
//font sizes
--text-xs: 11px;
--text-sm: 12px;
--text-md: 13px;
--text-base: 14px;
--text-lg: 16px;
--text-xl: 18px;
--text-2xl: 20px;
--text-3xl: 22px;
--text-on-blue: var(--blue-600);
--text-on-light-blue: var(--blue-500);
--text-on-dark-blue: var(--blue-700);

View file

@ -4,15 +4,6 @@ $input-height: 28px !default;
:root,
[data-theme="light"] {
--text-xs: 11px;
--text-sm: 12px;
--text-md: 13px;
--text-base: 14px;
--text-lg: 16px;
--text-xl: 18px;
--text-2xl: 20px;
--text-3xl: 22px;
// breakpoints
--xxl-width: map-get($grid-breakpoints, '2xl');
--xl-width: map-get($grid-breakpoints, 'xl');

View file

@ -1,7 +1,9 @@
@import "./desk/variables";
body {
background-color: var(--bg-light-gray);
@include media-breakpoint-up(sm) {
background-color: var(--bg-light-gray);
}
}
.for-forgot,

View file

@ -94,6 +94,8 @@
max-width: 300px;
border: 1px solid var(--dark-border-color);
box-shadow: none;
border-radius: var(--border-radius);
font-size: $font-size-sm;
}
}
}

View file

@ -27,6 +27,14 @@
@import 'navbar';
@import 'footer';
@import 'error-state';
@import 'my_account';
body {
@include media-breakpoint-up(sm) {
background-color: var(--bg-color);
}
}
.ql-editor.read-mode {
padding: 0;
@ -166,6 +174,10 @@ a.card {
font-size: inherit;
}
.indicator-pill {
font-size: var(--font-size-xs)
}
h4.modal-title {
font-size: 1em;
}
@ -298,3 +310,7 @@ h5.modal-title {
margin: 70px auto;
font-size: $font-size-sm;
}
.empty-list-icon {
height: 70px;
}

View file

@ -0,0 +1,116 @@
//styles for my account and edit-profile page
@include media-breakpoint-up(sm) {
body[data-path="me"],
body[data-path="list"] {
background-color: var(--bg-color);
}
}
@include media-breakpoint-down(sm) {
#page-me {
.side-list {
.list-group {
display: none;
}
}
}
}
.my-account-header {
color: var(--gray-900);
margin-bottom: var(--margin-lg);
font-weight: bold;
@include media-breakpoint-down(sm) {
margin-left: -1rem;
}
}
.account-info {
background-color: var(--fg-color);
border-radius: var(--border-radius-md);
padding: var(--padding-sm) 25px;
max-width: 850px;
@include media-breakpoint-up(sm) {
margin-left: 0;
}
@include media-breakpoint-down(sm) {
padding: 0;
}
.my-account-name,
.my-account-item {
color: var(--gray-900);
font-weight: var(--text-bold);
}
.my-account-avatar {
.avatar {
height: 60px;
width: 60px;
}
}
.my-account-item-desc {
color: var(--gray-700);
font-size: var(--text-md);
}
.my-account-item-link {
font-size: var(--text-md);
a {
text-decoration: none;
.edit-profile-icon {
stroke: var(--blue-500);
}
}
.right-icon {
@include media-breakpoint-up(sm) {
display: none;
}
}
.item-link-text {
@include media-breakpoint-down(sm) {
display: none;
}
}
}
.col {
padding: var(--padding-md) 0;
border-bottom: 1px solid var(--border-color);
.form-group {
margin-right: var(--margin-lg);
}
}
:last-child {
border: 0;
}
}
//styles for third party apps page
//center wrt to outer most container and not immediate parent
.empty-apps-state {
position: relative;
padding-top: 10rem;
margin-left: -250px;
text-align: center;
@include media-breakpoint-down(sm) {
margin: auto;
padding-top: 5rem;
}
@include media-breakpoint-down(md) {
margin-left: 0;
}
}

View file

@ -1,5 +1,31 @@
@import "../common/form";
[data-doctype="Web Form"] {
.page-content-wrapper {
.breadcrumb-container.container {
@include media-breakpoint-up(sm) {
padding-left: var(--padding-sm);
}
}
.container {
max-width: 800px;
&.my-4 {
background-color: var(--fg-color);
@include media-breakpoint-up(sm) {
padding: 1.8rem;
border-radius: var(--border-radius-md);
box-shadow: var(--card-shadow);
}
}
}
}
}
.web-form-wrapper {
.form-control {
color: var(--text-color);
@ -16,6 +42,7 @@
.form-column {
padding: 0 var(--padding-md);
&:first-child {
padding-left: 0;
}
@ -24,4 +51,24 @@
padding-right: 0;
}
}
}
.web-form-wrapper~#datatable {
.table {
thead {
th {
border: 0;
font-weight: normal;
color: var(--text-muted)
}
}
tr {
color: var(--text-color);
td {
border-top: 1px solid var(--border-color);
}
}
}
}

View file

@ -68,9 +68,14 @@ def get_sessions_to_clear(user=None, keep_current=False, device=None):
session = DocType("Sessions")
session_id = frappe.qb.from_(session).where((session.user == user) & (session.device.isin(device)))
if keep_current:
session_id = session_id.where(session.sid != frappe.db.escape(frappe.session.sid))
session_id = session_id.where(session.sid != frappe.session.sid)
query = session_id.select(session.sid).offset(offset).limit(100).orderby(session.lastupdate, order=Order.desc)
query = (
session_id.select(session.sid)
.offset(offset)
.limit(100)
.orderby(session.lastupdate, order=Order.desc)
)
return query.run(pluck=True)

View file

@ -2,8 +2,9 @@
<h4 class="text-muted">{{ sub_title }}</h4>
{% endif %}
{% if not result -%}
<div class="text-muted" style="min-height: 300px;">
{{ no_result_message or _("Nothing to show") }}
<div class="empty-apps-state">
<img class="empty-list-icon" src="/assets/frappe/images/ui-states/list-empty-state.svg"/>
<div class="mt-4">{{ no_result_message or _("Nothing to show") }}</div>
</div>
{% else %}
<div class="website-list" data-doctype="{{ doctype }}"

View file

@ -2,79 +2,87 @@
background-color: var(--bg-color);
}
body {
background-color: var(--bg-color);
}
.page-card {
max-width: 360px;
padding: 15px;
margin: 70px auto;
border-radius: 4px;
background-color: var(--fg-color);
box-shadow: var(--shadow-base);
max-width: 360px;
padding: 15px;
margin: 70px auto;
border-radius: 4px;
background-color: var(--fg-color);
/* box-shadow: var(--shadow-base); */
}
.for-reset-password {
margin: 80px 0;
}
margin: 80px 0;
}
.for-reset-password .page-card {
border: 0;
max-width: 450px;
margin: auto;
padding: 40px 60px;
border-radius: 10px;
box-shadow: var(--shadow-base);
border: 0;
max-width: 450px;
margin: auto;
border-radius: 10px;
}
@media (min-width: 567px) {
.for-reset-password .page-card {
box-shadow: var(--shadow-base);
padding: 40px 60px;
}
}
.page-card .page-card-head {
padding: 10px 15px;
margin: -15px;
margin-bottom: 15px;
border-bottom: 1px solid var(--border-color);
padding: 10px 15px;
margin: -15px;
margin-bottom: 15px;
border-bottom: 1px solid var(--border-color);
}
.for-reset-password .page-card .page-card-head {
border-bottom: 0;
.for-reset-password .page-card .page-card-head {
border-bottom: 0;
}
.page-card-head h4 {
font-size: 18px;
font-weight: 600;
font-size: 18px;
font-weight: 600;
}
#reset-password .form-group {
margin-bottom: 10px;
font-size: var(--font-size-sm);
margin-bottom: 10px;
font-size: var(--font-size-sm);
}
.page-card .page-card-head .indicator {
color: #36414C;
font-size: 14px;
color: #36414C;
font-size: 14px;
}
.sign-up-message {
margin-top: 20px;
font-size: 13px;
color: var(--text-color);
margin-top: 20px;
font-size: 13px;
color: var(--text-color);
}
.page-card .page-card-head .indicator::before {
margin: 0 6px 0.5px 0px;
margin: 0 6px 0.5px 0px;
}
button#update {
font-size: var(--font-size-sm);
font-size: var(--font-size-sm);
}
.page-card .btn {
margin-top: 30px;
margin-top: 30px;
}
.page-card p {
font-size: 14px;
font-size: 14px;
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
vertical-align: middle;
}
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
vertical-align: middle;
}

View file

@ -335,7 +335,10 @@ def make_test_records_for_doctype(doctype, verbose=0, force=False):
frappe.local.test_objects[doctype] += test_module._make_test_records(verbose)
elif hasattr(test_module, "test_records"):
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_module.test_records, verbose, force)
if doctype in frappe.local.test_objects:
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_module.test_records, verbose, force)
else:
frappe.local.test_objects[doctype] = make_test_objects(doctype, test_module.test_records, verbose, force)
else:
test_records = frappe.get_test_records(doctype)

View file

@ -4,38 +4,44 @@ import time
import unittest
import frappe
from frappe.auth import HTTPRequest, LoginAttemptTracker
import frappe.utils
from frappe.auth import LoginAttemptTracker
from frappe.frappeclient import FrappeClient, AuthError
from frappe.utils import set_request
def add_user(email, password, username=None, mobile_no=None):
first_name = email.split('@', 1)[0]
user = frappe.get_doc(
dict(doctype='User', email=email, first_name=first_name, username=username, mobile_no=mobile_no)
).insert()
user.new_password = password
user.add_roles("System Manager")
frappe.db.commit()
class TestAuth(unittest.TestCase):
def __init__(self, *args, **kwargs):
super(TestAuth, self).__init__(*args, **kwargs)
self.test_user_email = 'test_auth@test.com'
self.test_user_name = 'test_auth_user'
self.test_user_mobile = '+911234567890'
self.test_user_password = 'pwd_012'
@classmethod
def setUpClass(cls):
cls.HOST_NAME = (
frappe.get_site_config().host_name
or frappe.utils.get_site_url(frappe.local.site)
)
cls.test_user_email = 'test_auth@test.com'
cls.test_user_name = 'test_auth_user'
cls.test_user_mobile = '+911234567890'
cls.test_user_password = 'pwd_012'
def setUp(self):
self.tearDown()
cls.tearDownClass()
add_user(email=cls.test_user_email, password=cls.test_user_password,
username=cls.test_user_name, mobile_no=cls.test_user_mobile)
self.add_user(self.test_user_email, self.test_user_password,
username=self.test_user_name, mobile_no=self.test_user_mobile)
def tearDown(self):
frappe.delete_doc('User', self.test_user_email, force=True)
def add_user(self, email, password, username=None, mobile_no=None):
first_name = email.split('@', 1)[0]
user = frappe.get_doc(
dict(doctype='User', email=email, first_name=first_name, username=username, mobile_no=mobile_no)
).insert()
user.new_password = password
user.save()
frappe.db.commit()
@classmethod
def tearDownClass(cls):
frappe.delete_doc('User', cls.test_user_email, force=True)
def set_system_settings(self, k, v):
frappe.db.set_value("System Settings", "System Settings", k, v)
frappe.clear_cache()
frappe.db.commit()
def test_allow_login_using_mobile(self):
@ -43,12 +49,12 @@ class TestAuth(unittest.TestCase):
self.set_system_settings('allow_login_using_user_name', 0)
# Login by both email and mobile should work
FrappeClient(frappe.get_site_config().host_name, self.test_user_mobile, self.test_user_password)
FrappeClient(frappe.get_site_config().host_name, self.test_user_email, self.test_user_password)
FrappeClient(self.HOST_NAME, self.test_user_mobile, self.test_user_password)
FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password)
# login by username should fail
with self.assertRaises(AuthError):
FrappeClient(frappe.get_site_config().host_name, self.test_user_name, self.test_user_password)
FrappeClient(self.HOST_NAME, self.test_user_name, self.test_user_password)
def test_allow_login_using_only_email(self):
self.set_system_settings('allow_login_using_mobile_number', 0)
@ -56,14 +62,14 @@ class TestAuth(unittest.TestCase):
# Login by mobile number should fail
with self.assertRaises(AuthError):
FrappeClient(frappe.get_site_config().host_name, self.test_user_mobile, self.test_user_password)
FrappeClient(self.HOST_NAME, self.test_user_mobile, self.test_user_password)
# login by username should fail
with self.assertRaises(AuthError):
FrappeClient(frappe.get_site_config().host_name, self.test_user_name, self.test_user_password)
FrappeClient(self.HOST_NAME, self.test_user_name, self.test_user_password)
# Login by email should work
FrappeClient(frappe.get_site_config().host_name, self.test_user_email, self.test_user_password)
FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password)
def test_allow_login_using_username(self):
self.set_system_settings('allow_login_using_mobile_number', 0)
@ -71,20 +77,39 @@ class TestAuth(unittest.TestCase):
# Mobile login should fail
with self.assertRaises(AuthError):
FrappeClient(frappe.get_site_config().host_name, self.test_user_mobile, self.test_user_password)
FrappeClient(self.HOST_NAME, self.test_user_mobile, self.test_user_password)
# Both email and username logins should work
FrappeClient(frappe.get_site_config().host_name, self.test_user_email, self.test_user_password)
FrappeClient(frappe.get_site_config().host_name, self.test_user_name, self.test_user_password)
FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password)
FrappeClient(self.HOST_NAME, self.test_user_name, self.test_user_password)
def test_allow_login_using_username_and_mobile(self):
self.set_system_settings('allow_login_using_mobile_number', 1)
self.set_system_settings('allow_login_using_user_name', 1)
# Both email and username and mobile logins should work
FrappeClient(frappe.get_site_config().host_name, self.test_user_mobile, self.test_user_password)
FrappeClient(frappe.get_site_config().host_name, self.test_user_email, self.test_user_password)
FrappeClient(frappe.get_site_config().host_name, self.test_user_name, self.test_user_password)
FrappeClient(self.HOST_NAME, self.test_user_mobile, self.test_user_password)
FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password)
FrappeClient(self.HOST_NAME, self.test_user_name, self.test_user_password)
def test_deny_multiple_login(self):
self.set_system_settings('deny_multiple_sessions', 1)
first_login = FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password)
first_login.get_list("ToDo")
second_login = FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password)
second_login.get_list("ToDo")
with self.assertRaises(Exception):
first_login.get_list("ToDo")
third_login = FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password)
with self.assertRaises(Exception):
first_login.get_list("ToDo")
with self.assertRaises(Exception):
second_login.get_list("ToDo")
third_login.get_list("ToDo")
class TestLoginAttemptTracker(unittest.TestCase):
def test_account_lock(self):

View file

@ -1,4 +1,5 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
# imports - standard imports
import gzip
@ -9,13 +10,14 @@ import shutil
import subprocess
from typing import List
import unittest
import glob
from glob import glob
from unittest.case import skipIf
# imports - module imports
import frappe
import frappe.recorder
from frappe.installer import add_to_installed_apps, remove_app
from frappe.utils import add_to_date, get_bench_relative_path, now
from frappe.utils import add_to_date, get_bench_path, get_bench_relative_path, now
from frappe.utils.backups import fetch_latest_backups
# imports - third party imports
@ -133,134 +135,6 @@ class TestCommands(BaseTestCommands):
self.assertEqual(self.returncode, 0)
self.assertEqual(self.stdout[1:-1], frappe.bold(text="DocType"))
def test_backup(self):
backup = {
"includes": {
"includes": [
"ToDo",
"Note",
]
},
"excludes": {
"excludes": [
"Activity Log",
"Access Log",
"Error Log"
]
}
}
home = os.path.expanduser("~")
site_backup_path = frappe.utils.get_site_path("private", "backups")
# test 1: take a backup
before_backup = fetch_latest_backups()
self.execute("bench --site {site} backup")
after_backup = fetch_latest_backups()
self.assertEqual(self.returncode, 0)
self.assertIn("successfully completed", self.stdout)
self.assertNotEqual(before_backup["database"], after_backup["database"])
# test 2: take a backup with --with-files
before_backup = after_backup.copy()
self.execute("bench --site {site} backup --with-files")
after_backup = fetch_latest_backups()
self.assertEqual(self.returncode, 0)
self.assertIn("successfully completed", self.stdout)
self.assertIn("with files", self.stdout)
self.assertNotEqual(before_backup, after_backup)
self.assertIsNotNone(after_backup["public"])
self.assertIsNotNone(after_backup["private"])
# test 3: take a backup with --backup-path
backup_path = os.path.join(home, "backups")
self.execute("bench --site {site} backup --backup-path {backup_path}", {"backup_path": backup_path})
self.assertEqual(self.returncode, 0)
self.assertTrue(os.path.exists(backup_path))
self.assertGreaterEqual(len(os.listdir(backup_path)), 2)
# test 4: take a backup with --backup-path-db, --backup-path-files, --backup-path-private-files, --backup-path-conf
kwargs = {
key: os.path.join(home, key, value)
for key, value in {
"db_path": "database.sql.gz",
"files_path": "public.tar",
"private_path": "private.tar",
"conf_path": "config.json",
}.items()
}
self.execute(
"""bench
--site {site} backup --with-files
--backup-path-db {db_path}
--backup-path-files {files_path}
--backup-path-private-files {private_path}
--backup-path-conf {conf_path}""",
kwargs,
)
self.assertEqual(self.returncode, 0)
for path in kwargs.values():
self.assertTrue(os.path.exists(path))
# test 5: take a backup with --compress
self.execute("bench --site {site} backup --with-files --compress")
self.assertEqual(self.returncode, 0)
compressed_files = glob.glob(site_backup_path + "/*.tgz")
self.assertGreater(len(compressed_files), 0)
# test 6: take a backup with --verbose
self.execute("bench --site {site} backup --verbose")
self.assertEqual(self.returncode, 0)
# test 7: take a backup with frappe.conf.backup.includes
self.execute(
"bench --site {site} set-config backup '{includes}' --parse",
{"includes": json.dumps(backup["includes"])},
)
self.execute("bench --site {site} backup --verbose")
self.assertEqual(self.returncode, 0)
database = fetch_latest_backups(partial=True)["database"]
self.assertEqual([], missing_in_backup(backup["includes"]["includes"], database))
# test 8: take a backup with frappe.conf.backup.excludes
self.execute(
"bench --site {site} set-config backup '{excludes}' --parse",
{"excludes": json.dumps(backup["excludes"])},
)
self.execute("bench --site {site} backup --verbose")
self.assertEqual(self.returncode, 0)
database = fetch_latest_backups(partial=True)["database"]
self.assertFalse(exists_in_backup(backup["excludes"]["excludes"], database))
self.assertEqual([], missing_in_backup(backup["includes"]["includes"], database))
# test 9: take a backup with --include (with frappe.conf.excludes still set)
self.execute(
"bench --site {site} backup --include '{include}'",
{"include": ",".join(backup["includes"]["includes"])},
)
self.assertEqual(self.returncode, 0)
database = fetch_latest_backups(partial=True)["database"]
self.assertEqual([], missing_in_backup(backup["includes"]["includes"], database))
# test 10: take a backup with --exclude
self.execute(
"bench --site {site} backup --exclude '{exclude}'",
{"exclude": ",".join(backup["excludes"]["excludes"])},
)
self.assertEqual(self.returncode, 0)
database = fetch_latest_backups(partial=True)["database"]
self.assertFalse(exists_in_backup(backup["excludes"]["excludes"], database))
# test 11: take a backup with --ignore-backup-conf
self.execute("bench --site {site} backup --ignore-backup-conf")
self.assertEqual(self.returncode, 0)
database = fetch_latest_backups()["database"]
self.assertEqual([], missing_in_backup(backup["excludes"]["excludes"], database))
def test_restore(self):
# step 0: create a site to run the test on
global_config = {
@ -405,7 +279,7 @@ class TestCommands(BaseTestCommands):
self.assertIsInstance(json.loads(self.stdout), dict)
def test_get_bench_relative_path(self):
bench_path = frappe.utils.get_bench_path()
bench_path = get_bench_path()
test1_path = os.path.join(bench_path, "test1.txt")
test2_path = os.path.join(bench_path, "sites", "test2.txt")
@ -463,7 +337,7 @@ class TestCommands(BaseTestCommands):
b"MIT" # app_license
]
app_name = "testapp0"
apps_path = os.path.join(frappe.utils.get_bench_path(), "apps")
apps_path = os.path.join(get_bench_path(), "apps")
test_app_path = os.path.join(apps_path, app_name)
self.execute(f"bench make-app {apps_path} {app_name}", {"cmd_input": b'\n'.join(user_input)})
self.assertEqual(self.returncode, 0)
@ -474,26 +348,199 @@ class TestCommands(BaseTestCommands):
# cleanup
shutil.rmtree(test_app_path)
def disable_test_bench_drop_site_should_archive_site(self):
@skipIf(
not (
frappe.conf.root_password
and frappe.conf.admin_password
and frappe.conf.db_type == "mariadb"
),
"DB Root password and Admin password not set in config"
)
def test_bench_drop_site_should_archive_site(self):
# TODO: Make this test postgres compatible
site = 'test_site.localhost'
self.execute(
f"bench new-site {site} --force --verbose --admin-password {frappe.conf.admin_password} "
f"--mariadb-root-password {frappe.conf.root_password}"
f"bench new-site {site} --force --verbose "
f"--admin-password {frappe.conf.admin_password} "
f"--mariadb-root-password {frappe.conf.root_password} "
f"--db-type {frappe.conf.db_type or 'mariadb'} "
)
self.assertEqual(self.returncode, 0)
self.execute(f"bench drop-site {site} --force --root-password {frappe.conf.root_password}")
self.assertEqual(self.returncode, 0)
bench_path = frappe.utils.get_bench_path()
bench_path = get_bench_path()
site_directory = os.path.join(bench_path, f'sites/{site}')
self.assertFalse(os.path.exists(site_directory))
archive_directory = os.path.join(bench_path, f'archived/sites/{site}')
self.assertTrue(os.path.exists(archive_directory))
class RemoveAppUnitTests(unittest.TestCase):
class TestBackups(BaseTestCommands):
backup_map = {
"includes": {
"includes": [
"ToDo",
"Note",
]
},
"excludes": {
"excludes": [
"Activity Log",
"Access Log",
"Error Log"
]
}
}
home = os.path.expanduser("~")
site_backup_path = frappe.utils.get_site_path("private", "backups")
def setUp(self):
self.files_to_trash = []
def tearDown(self):
if self._testMethodName == "test_backup":
for file in self.files_to_trash:
os.remove(file)
try:
os.rmdir(os.path.dirname(file))
except OSError:
pass
def test_backup_no_options(self):
"""Take a backup without any options
"""
before_backup = fetch_latest_backups(partial=True)
self.execute("bench --site {site} backup")
after_backup = fetch_latest_backups(partial=True)
self.assertEqual(self.returncode, 0)
self.assertIn("successfully completed", self.stdout)
self.assertNotEqual(before_backup["database"], after_backup["database"])
def test_backup_with_files(self):
"""Take a backup with files (--with-files)
"""
before_backup = fetch_latest_backups()
self.execute("bench --site {site} backup --with-files")
after_backup = fetch_latest_backups()
self.assertEqual(self.returncode, 0)
self.assertIn("successfully completed", self.stdout)
self.assertIn("with files", self.stdout)
self.assertNotEqual(before_backup, after_backup)
self.assertIsNotNone(after_backup["public"])
self.assertIsNotNone(after_backup["private"])
def test_backup_with_custom_path(self):
"""Backup to a custom path (--backup-path)
"""
backup_path = os.path.join(self.home, "backups")
self.execute("bench --site {site} backup --backup-path {backup_path}", {"backup_path": backup_path})
self.assertEqual(self.returncode, 0)
self.assertTrue(os.path.exists(backup_path))
self.assertGreaterEqual(len(os.listdir(backup_path)), 2)
def test_backup_with_different_file_paths(self):
"""Backup with different file paths (--backup-path-db, --backup-path-files, --backup-path-private-files, --backup-path-conf)
"""
kwargs = {
key: os.path.join(self.home, key, value)
for key, value in {
"db_path": "database.sql.gz",
"files_path": "public.tar",
"private_path": "private.tar",
"conf_path": "config.json",
}.items()
}
self.execute(
"""bench
--site {site} backup --with-files
--backup-path-db {db_path}
--backup-path-files {files_path}
--backup-path-private-files {private_path}
--backup-path-conf {conf_path}""",
kwargs,
)
self.assertEqual(self.returncode, 0)
for path in kwargs.values():
self.assertTrue(os.path.exists(path))
def test_backup_compress_files(self):
"""Take a compressed backup (--compress)
"""
self.execute("bench --site {site} backup --with-files --compress")
self.assertEqual(self.returncode, 0)
compressed_files = glob(f"{self.site_backup_path}/*.tgz")
self.assertGreater(len(compressed_files), 0)
def test_backup_verbose(self):
"""Take a verbose backup (--verbose)
"""
self.execute("bench --site {site} backup --verbose")
self.assertEqual(self.returncode, 0)
def test_backup_only_specific_doctypes(self):
"""Take a backup with (include) backup options set in the site config `frappe.conf.backup.includes`
"""
self.execute(
"bench --site {site} set-config backup '{includes}' --parse",
{"includes": json.dumps(self.backup_map["includes"])},
)
self.execute("bench --site {site} backup --verbose")
self.assertEqual(self.returncode, 0)
database = fetch_latest_backups(partial=True)["database"]
self.assertEqual([], missing_in_backup(self.backup_map["includes"]["includes"], database))
def test_backup_excluding_specific_doctypes(self):
"""Take a backup with (exclude) backup options set (`frappe.conf.backup.excludes`, `--exclude`)
"""
# test 1: take a backup with frappe.conf.backup.excludes
self.execute(
"bench --site {site} set-config backup '{excludes}' --parse",
{"excludes": json.dumps(self.backup_map["excludes"])},
)
self.execute("bench --site {site} backup --verbose")
self.assertEqual(self.returncode, 0)
database = fetch_latest_backups(partial=True)["database"]
self.assertFalse(exists_in_backup(self.backup_map["excludes"]["excludes"], database))
self.assertEqual([], missing_in_backup(self.backup_map["includes"]["includes"], database))
# test 2: take a backup with --exclude
self.execute(
"bench --site {site} backup --exclude '{exclude}'",
{"exclude": ",".join(self.backup_map["excludes"]["excludes"])},
)
self.assertEqual(self.returncode, 0)
database = fetch_latest_backups(partial=True)["database"]
self.assertFalse(exists_in_backup(self.backup_map["excludes"]["excludes"], database))
def test_selective_backup_priority_resolution(self):
"""Take a backup with conflicting backup options set (`frappe.conf.excludes`, `--include`)
"""
self.execute(
"bench --site {site} backup --include '{include}'",
{"include": ",".join(self.backup_map["includes"]["includes"])},
)
self.assertEqual(self.returncode, 0)
database = fetch_latest_backups(partial=True)["database"]
self.assertEqual([], missing_in_backup(self.backup_map["includes"]["includes"], database))
def test_dont_backup_conf(self):
"""Take a backup ignoring frappe.conf.backup settings (with --ignore-backup-conf option)
"""
self.execute("bench --site {site} backup --ignore-backup-conf")
self.assertEqual(self.returncode, 0)
database = fetch_latest_backups()["database"]
self.assertEqual([], missing_in_backup(self.backup_map["excludes"]["excludes"], database))
class TestRemoveApp(unittest.TestCase):
def test_delete_modules(self):
from frappe.installer import (
_delete_doctypes,

View file

@ -1,6 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe, unittest
import frappe
import datetime
import unittest
from frappe.model.db_query import DatabaseQuery
from frappe.desk.reportview import get_filters_cond
@ -380,6 +382,22 @@ class TestReportview(unittest.TestCase):
owners = DatabaseQuery("DocType").execute(filters={"name": "DocType"}, pluck="owner")
self.assertEqual(owners, ["Administrator"])
def test_prepare_select_args(self):
# frappe.get_all inserts modified field into order_by clause
# test to make sure this is inserted into select field when postgres
doctypes = frappe.get_all("DocType",
filters={"docstatus": 0, "document_type": ("!=", "")},
group_by="document_type",
fields=["document_type", "sum(is_submittable) as is_submittable"],
limit=1,
as_list=True,
)
if frappe.conf.db_type == "mariadb":
self.assertTrue(len(doctypes[0]) == 2)
else:
self.assertTrue(len(doctypes[0]) == 3)
self.assertTrue(isinstance(doctypes[0][2], datetime.datetime))
def test_column_comparison(self):
"""Test DatabaseQuery.execute to test column comparison
"""

View file

@ -252,3 +252,8 @@ class TestDocument(unittest.TestCase):
'currency': 100000
})
self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00')
def test_limit_for_get(self):
doc = frappe.get_doc("DocType", "DocType")
# assuming DocType has more that 3 Data fields
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3)

View file

@ -197,6 +197,7 @@ class TestWebsite(unittest.TestCase):
frappe.cache().delete_key('app_hooks')
def test_printview_page(self):
frappe.db.value_cache[('DocType', 'Language', 'name')] = (('Language',),)
content = get_response_content('/Language/ru')
self.assertIn('<div class="print-format">', content)
self.assertIn('<div>Language</div>', content)

View file

@ -56,7 +56,7 @@ def get_email_address(user=None):
def get_formatted_email(user, mail=None):
"""get Email Address of user formatted as: `John Doe <johndoe@example.com>`"""
fullname = get_fullname(user)
method = get_hook_method('get_sender_details')
if method:
sender_name, mail = method()
@ -623,12 +623,11 @@ def get_installed_apps_info():
return out
def get_site_info():
from frappe.core.doctype.user.user import STANDARD_USERS
from frappe.email.queue import get_emails_sent_this_month
from frappe.utils.user import get_system_managers
# only get system users
users = frappe.get_all('User', filters={'user_type': 'System User', 'name': ('not in', STANDARD_USERS)},
users = frappe.get_all('User', filters={'user_type': 'System User', 'name': ('not in', frappe.STANDARD_USERS)},
fields=['name', 'enabled', 'last_login', 'last_active', 'language', 'time_zone'])
system_managers = get_system_managers(only_name=True)
for u in users:
@ -898,3 +897,14 @@ def dictify(arg):
arg = frappe._dict(arg)
return arg
def add_user_info(user, user_info):
if user not in user_info:
info = frappe.db.get_value("User",
user, ["full_name", "user_image", "name", 'email'], as_dict=True) or frappe._dict()
user_info[user] = frappe._dict(
fullname = info.full_name or user,
image = info.user_image,
name = user,
email = info.email
)

View file

@ -113,6 +113,9 @@ def get_timedelta(time: Optional[str] = None) -> Optional[datetime.timedelta]:
def to_timedelta(time_str):
from dateutil import parser
if isinstance(time_str, datetime.time):
time_str = str(time_str)
if isinstance(time_str, str):
t = parser.parse(time_str)
return datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond)

View file

@ -219,6 +219,12 @@ def add_standard_navbar_items():
'action': 'frappe.ui.toolbar.toggle_full_width()',
'is_standard': 1
},
{
'item_label': 'Toggle Theme',
'item_type': 'Action',
'action': 'new frappe.ui.ThemeSwitcher().show()',
'is_standard': 1
},
{
'item_label': 'Background Jobs',
'item_type': 'Route',

View file

@ -35,9 +35,13 @@ def get_random(doctype, filters=None, doc=False):
condition = " where " + " and ".join(condition)
else:
condition = ""
out = frappe.db.sql("""select name from `tab%s` %s
order by RAND() limit 0,1""" % (doctype, condition))
out = frappe.db.multisql({
'mariadb': """select name from `tab%s` %s
order by RAND() limit 1 offset 0""" % (doctype, condition),
'postgres': """select name from `tab%s` %s
order by RANDOM() limit 1 offset 0""" % (doctype, condition)
})
out = out and out[0][0] or None

View file

@ -1,4 +1,4 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
# Tree (Hierarchical) Nested Set Model (nsm)
@ -109,7 +109,6 @@ def update_move_node(doc, parent_field):
new_parent = frappe.db.sql("""select lft, rgt from `tab%s`
where name = %s for update""" % (doc.doctype, '%s'), parent, as_dict=1)[0]
# set parent lft, rgt
frappe.db.sql("""update `tab{0}` set rgt = rgt + %s
where name = %s""".format(doc.doctype), (diff, parent))
@ -134,6 +133,7 @@ def update_move_node(doc, parent_field):
frappe.db.sql("""update `tab{0}` set lft = -lft + %s, rgt = -rgt + %s
where lft < 0""".format(doc.doctype), (new_diff, new_diff))
@frappe.whitelist()
def rebuild_tree(doctype, parent_field):
"""
@ -153,7 +153,6 @@ def rebuild_tree(doctype, parent_field):
right = 1
table = DocType(doctype)
column = getattr(table, parent_field)
result = (
frappe.qb.from_(table)
.where(

View file

@ -125,7 +125,7 @@ def json_handler(obj):
# serialize date
import collections.abc
if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime)):
if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime, datetime.time)):
return str(obj)
elif isinstance(obj, decimal.Decimal):

View file

@ -17,7 +17,6 @@ import schedule
# imports - module imports
import frappe
from frappe.core.doctype.user.user import STANDARD_USERS
from frappe.installer import update_site_config
from frappe.utils import get_sites, now_datetime
from frappe.utils.background_jobs import get_jobs

View file

@ -230,7 +230,6 @@ def get_fullname_and_avatar(user):
def get_system_managers(only_name=False):
"""returns all system manager's user details"""
import email.utils
from frappe.core.doctype.user.user import STANDARD_USERS
system_managers = frappe.db.sql("""SELECT DISTINCT `name`, `creation`,
CONCAT_WS(' ',
CASE WHEN `first_name`= '' THEN NULL ELSE `first_name` END,
@ -245,8 +244,8 @@ def get_system_managers(only_name=False):
FROM `tabHas Role` AS ur
WHERE ur.parent = p.name
AND ur.role='System Manager')
ORDER BY `creation` DESC""".format(", ".join(["%s"]*len(STANDARD_USERS))),
STANDARD_USERS, as_dict=True)
ORDER BY `creation` DESC""".format(", ".join(["%s"]*len(frappe.STANDARD_USERS))),
frappe.STANDARD_USERS, as_dict=True)
if only_name:
return [p.name for p in system_managers]

View file

@ -58,15 +58,18 @@ class TestBlogPost(unittest.TestCase):
category_page_link = list(soup.find_all('a', href=re.compile(blog.blog_category)))[0]
category_page_url = category_page_link["href"]
cached_value = frappe.db.value_cache[('DocType', 'Blog Post', 'name')]
frappe.db.value_cache[('DocType', 'Blog Post', 'name')] = (('Blog Post',),)
# Visit the category page (by following the link found in above stage)
set_request(path=category_page_url)
category_page_response = get_response()
category_page_html = frappe.safe_decode(category_page_response.get_data())
# Category page should contain the blog post title
self.assertIn(blog.title, category_page_html)
# Cleanup
frappe.db.value_cache[('DocType', 'Blog Post', 'name')] = cached_value
frappe.delete_doc("Blog Post", blog.name)
frappe.delete_doc("Blog Category", blog.blog_category)

View file

@ -3,7 +3,7 @@
{% block title %}{{ _(title) }}{% endblock %}
{% block header %}
<h2>{{ _(title) }}</h2>
<h3>{{ _(title) }}</h3>
{% endblock %}
{% block breadcrumbs %}
@ -29,8 +29,8 @@ data-web-form="{{ name }}" data-web-form-doctype="{{ doc_type }}" data-login-req
{% if is_list %}
{# web form list #}
<div class="web-form-wrapper" {{ container_attributes() }}></div>
<div id="list-filters" class="row"></div>
<div id="datatable" class="pt-4"></div>
<div id="list-filters" class="row mt-4"></div>
<div id="datatable" class="pt-4 overflow-auto"></div>
<div class="list-view-footer text-right"></div>
{% else %}
{# web form #}
@ -38,7 +38,7 @@ data-web-form="{{ name }}" data-web-form-doctype="{{ doc_type }}" data-login-req
<div id="introduction" class="text-muted"></div>
<hr>
<div class="web-form-wrapper" {{ container_attributes() }}></div>
<div class="web-form-footer pull-right"></div>
<div class="web-form-footer text-right"></div>
</div>
{% if show_attachments and not frappe.form_dict.new and attachments %}

View file

@ -66,7 +66,7 @@ class TestWebForm(unittest.TestCase):
def test_webform_render(self):
content = get_response_content('request-data')
self.assertIn('<h2>Request Data</h2>', content)
self.assertIn('<h3>Request Data</h3>', content)
self.assertIn('data-doctype="Web Form"', content)
self.assertIn('data-path="request-data"', content)
self.assertIn('source-type="Generator"', content)

View file

@ -5,7 +5,7 @@
{% endblock %}
{% block header %}
<h1>{{ title or (_("{0} List").format(_(doctype))) }}</h1>
<h3 class="my-account-header">{{ title or (_("{0} List").format(_(doctype))) }}</h3>
{% endblock %}
{% block breadcrumbs %}
@ -23,11 +23,9 @@
{% endblock %}
{% block page_content %}
{% if introduction %}<p>{{ introduction }}</p>{% endif %}
{% include list_template or "templates/includes/list/list.html" %}
{% if list_footer %}{{ list_footer }}{% endif %}
{% if introduction %}<p>{{ introduction }}</p>{% endif %}
{% include list_template or "templates/includes/list/list.html" %}
{% if list_footer %}{{ list_footer }}{% endif %}
{% endblock %}
{% block script %}

View file

@ -1,31 +1,94 @@
{% from "frappe/templates/includes/avatar_macro.html" import avatar %}
{% extends "templates/web.html" %}
{% block title %}{{ _("My Account") }}{% endblock %}
{% block header %}<h1>{{ _("My Account") }}</h1>{% endblock %}
{% block title %}
{{ _("My Account") }}
{% endblock %}
{% block header %}
<h3 class="my-account-header">{{_("My Account") }}</h3>
{% endblock %}
{% block page_content %}
<div class="row your-account-info d-none d-sm-block">
<div class="col-sm-4">
<ul class="list-unstyled">
<li><a href="/update-password">{{ _("Reset Password") }}</a></li>
<li><a href="/update-profile?name={{ user }}">{{ _("Edit Profile") }}</a></li>
<li><a href="/third_party_apps">{{ _("Manage Third Party Apps") }}</a></li>
{% if frappe.db.get_single_value("Website Settings", "show_account_deletion_link") %}
<li><a href="/request-for-account-deletion?new=1">{{ _("Request for Account Deletion") }}</a></li>
{% endif %}
</ul>
<div class="row account-info d-flex flex-column">
<div class="col d-flex justify-content-between align-items-center">
<div>
<span class="my-account-avatar">
{{avatar(current_user.name)}}
</span>
<span class="my-account-name ml-4">
{{current_user.full_name }}
</span>
</div>
<div>
<span class="my-account-item-link">
<a href="/update-profile?name={{ user }}">
<svg class="edit-profile-icon icon icon-md">
<use xlink:href="#icon-edit">
</use>
</svg>
<span class="item-link-text pl-2">
{{_("Edit Profile") }}
</span>
</a>
</span>
</div>
</div>
<div class="col d-flex justify-content-between align-items-center">
<span>
<div class="my-account-item">{{_("Reset Password") }}</div>
<div class="my-account-item-desc">{{_("Reset the password for your account") }}</div>
</span>
<span class="my-account-item-link">
<a href="/update-password">
<svg class="right-icon icon icon-md">
<use xlink:href="#icon-right">
</use>
</svg>
<span class="item-link-text">{{_("Reset Password") }}</span>
</a>
</span>
</div>
<div class="col d-flex justify-content-between align-items-center">
<span>
<div class="my-account-item">{{_("Manage third party apps") }}</div>
<div class="my-account-item-desc">{{_("To manage your authorized third party apps") }}</div>
</span>
<span class="my-account-item-link">
<a href="/third_party_apps">
<svg class="right-icon icon icon-md">
<use xlink:href="#icon-right">
</use>
</svg>
<span class="item-link-text">{{_("Manage your apps") }}</span>
</a>
</span>
</div>
{% if frappe.db.get_single_value("Website Settings", "show_account_deletion_link") %}
<div class="col d-flex justify-content-between align-items-center">
<span>
<div class="my-account-item">{{_("Request Account Deletion") }}</div>
<div class="my-account-item-desc">{{_("Send a request to delete your account") }}</div>
</span>
<span class="my-account-item-link">
<a href="/request-for-account-deletion?new=1">
<svg class="right-icon icon icon-md">
<use xlink:href="#icon-right">
</use>
</svg>
<span class="item-link-text">{{_("Delete Account") }}</span>
</a>
</span>
</div>
{% endif %}
</div>
<div class="row d-block d-sm-none">
<div class="col-12">
<div class="col-12 side-list">
<ul class="list-group">
{% for item in sidebar_items -%}
<a class="list-group-item" href="{{ item.route }}"
{% if item.target %}target="{{ item.target }}"{% endif %}>
{{ _(item.title or item.label) }}
</a>
{%- endfor %}
{%- endfor %}
</ul>
</div>
</div>
{% endblock %}
{% endblock %}

View file

@ -10,5 +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

View file

@ -2,7 +2,7 @@
{% block title %} {{ _("Third Party Apps") }} {% endblock %}
{% block header %}
<h1>{{ _("Third Party Apps") }}</h1>
<h3 class="my-account-header">{{ _("Third Party Apps") }}</h3>
{% endblock %}
{% block page_sidebar %}
@ -52,9 +52,15 @@
</div>
{% endfor %}
{% else %}
<div class="text-muted">
<div class="empty-apps-state">
<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">
{{ _("Looks like you havent added any third party apps.")}}
</div>
</div>
{% endif %}
<div class="padding"></div>
<script>

View file

@ -57,5 +57,5 @@ setup(
{
'clean': CleanCommand
},
python_requires='>=3.7'
python_requires='>=3.8'
)