Merge branch 'develop' of https://github.com/frappe/frappe into link_title_refactor
This commit is contained in:
commit
30ba577c0a
66 changed files with 1059 additions and 449 deletions
2
.github/workflows/docs-checker.yml
vendored
2
.github/workflows/docs-checker.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
105
frappe/core/doctype/data_export/test_data_exporter.py
Normal file
105
frappe/core/doctype/data_export/test_data_exporter.py
Normal 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
|
||||
|
||||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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("%", "%%")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
6
frappe/public/images/ui-states/empty-app-state.svg
Normal file
6
frappe/public/images/ui-states/empty-app-state.svg
Normal 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 |
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
116
frappe/public/scss/website/my_account.scss
Normal file
116
frappe/public/scss/website/my_account.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }}"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 haven’t added any third party apps.")}}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="padding"></div>
|
||||
<script>
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -57,5 +57,5 @@ setup(
|
|||
{
|
||||
'clean': CleanCommand
|
||||
},
|
||||
python_requires='>=3.7'
|
||||
python_requires='>=3.8'
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue