Merge branch 'version-13-pre-release' into version-13
This commit is contained in:
commit
1419a28e5d
79 changed files with 1721 additions and 1201 deletions
18
.github/workflows/ci-tests.yml
vendored
18
.github/workflows/ci-tests.yml
vendored
|
|
@ -144,8 +144,8 @@ jobs:
|
|||
DB: ${{ matrix.DB }}
|
||||
TYPE: ${{ matrix.TYPE }}
|
||||
|
||||
- name: Coverage
|
||||
if: matrix.TYPE == 'server'
|
||||
- name: Coverage - Pull Request
|
||||
if: matrix.TYPE == 'server' && github.event_name == 'pull_request'
|
||||
run: |
|
||||
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
|
||||
cd ${GITHUB_WORKSPACE}
|
||||
|
|
@ -156,3 +156,17 @@ jobs:
|
|||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
|
||||
COVERALLS_SERVICE_NAME: github
|
||||
|
||||
- name: Coverage - Push
|
||||
if: matrix.TYPE == 'server' && github.event_name == 'push'
|
||||
run: |
|
||||
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
|
||||
cd ${GITHUB_WORKSPACE}
|
||||
pip install coveralls==2.2.0
|
||||
pip install coverage==4.5.4
|
||||
coveralls --service=github-actions
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
|
||||
COVERALLS_SERVICE_NAME: github-actions
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,10 @@ be used to build database driven apps.
|
|||
|
||||
Read the documentation: https://frappeframework.com/docs
|
||||
"""
|
||||
from __future__ import unicode_literals, print_function
|
||||
|
||||
from six import iteritems, binary_type, text_type, string_types, PY2
|
||||
from six import iteritems, binary_type, text_type, string_types
|
||||
from werkzeug.local import Local, release_local
|
||||
import os, sys, importlib, inspect, json
|
||||
import os, sys, importlib, inspect, json, warnings
|
||||
import typing
|
||||
from past.builtins import cmp
|
||||
import click
|
||||
|
|
@ -27,19 +26,14 @@ from .utils.lazy_loader import lazy_import
|
|||
# Lazy imports
|
||||
faker = lazy_import('faker')
|
||||
|
||||
|
||||
# Harmless for Python 3
|
||||
# For Python 2 set default encoding to utf-8
|
||||
if PY2:
|
||||
reload(sys)
|
||||
sys.setdefaultencoding("utf-8")
|
||||
|
||||
__version__ = '13.2.2'
|
||||
__version__ = '13.3.0'
|
||||
|
||||
__title__ = "Frappe Framework"
|
||||
|
||||
local = Local()
|
||||
controllers = {}
|
||||
warnings.simplefilter('always', DeprecationWarning)
|
||||
warnings.simplefilter('always', PendingDeprecationWarning)
|
||||
|
||||
class _dict(dict):
|
||||
"""dict like object that exposes keys as attributes"""
|
||||
|
|
|
|||
|
|
@ -294,6 +294,7 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No
|
|||
_sites_path = sites_path
|
||||
|
||||
from werkzeug.serving import run_simple
|
||||
patch_werkzeug_reloader()
|
||||
|
||||
if profile:
|
||||
application = ProfilerMiddleware(application, sort_by=('cumtime', 'calls'))
|
||||
|
|
@ -324,3 +325,23 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No
|
|||
use_debugger=not in_test_env,
|
||||
use_evalex=not in_test_env,
|
||||
threaded=not no_threading)
|
||||
|
||||
def patch_werkzeug_reloader():
|
||||
"""
|
||||
This function monkey patches Werkzeug reloader to ignore reloading files in
|
||||
the __pycache__ directory.
|
||||
|
||||
To be deprecated when upgrading to Werkzeug 2.
|
||||
"""
|
||||
|
||||
from werkzeug._reloader import WatchdogReloaderLoop
|
||||
|
||||
trigger_reload = WatchdogReloaderLoop.trigger_reload
|
||||
|
||||
def custom_trigger_reload(self, filename):
|
||||
if os.path.basename(os.path.dirname(filename)) == "__pycache__":
|
||||
return
|
||||
|
||||
return trigger_reload(self, filename)
|
||||
|
||||
WatchdogReloaderLoop.trigger_reload = custom_trigger_reload
|
||||
|
|
|
|||
|
|
@ -42,8 +42,6 @@ def get_bootinfo():
|
|||
bootinfo.user_info = get_user_info()
|
||||
bootinfo.sid = frappe.session['sid']
|
||||
|
||||
bootinfo.user_groups = frappe.get_all('User Group', pluck="name")
|
||||
|
||||
bootinfo.modules = {}
|
||||
bootinfo.module_list = []
|
||||
load_desktop_data(bootinfo)
|
||||
|
|
|
|||
49
frappe/change_log/v13/v13_3_0.md
Normal file
49
frappe/change_log/v13/v13_3_0.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Version 13.3.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- Deletion Steps in Data Deletion Tool ([#13124](https://github.com/frappe/frappe/pull/13124))
|
||||
- Format Option for list-apps in bench CLI ([#13125](https://github.com/frappe/frappe/pull/13125))
|
||||
- Add password fieldtype option for Web Form ([#13093](https://github.com/frappe/frappe/pull/13093))
|
||||
- Add simple __repr__ for DocTypes ([#13151](https://github.com/frappe/frappe/pull/13151))
|
||||
- Switch theme with left/right keys ([#13077](https://github.com/frappe/frappe/pull/13077))
|
||||
- sourceURL for injected javascript ([#13022](https://github.com/frappe/frappe/pull/13022))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Decode uri before importing file via weblink ([#13026](https://github.com/frappe/frappe/pull/13026))
|
||||
- Respond to /api requests as JSON by default ([#13053](https://github.com/frappe/frappe/pull/13053))
|
||||
- Disabled checkbox should be disabled ([#13021](https://github.com/frappe/frappe/pull/13021))
|
||||
- Moving Site folder across different FileSystems failed ([#13038](https://github.com/frappe/frappe/pull/13038))
|
||||
- Freeze screen till the background request is complete ([#13078](https://github.com/frappe/frappe/pull/13078))
|
||||
- Added conditional rendering for content field in split section w… ([#13075](https://github.com/frappe/frappe/pull/13075))
|
||||
- Show delete button on portal if user has permission to delete document ([#13149](https://github.com/frappe/frappe/pull/13149))
|
||||
- Dont disable dialog scroll on focusing a Link/Autocomplete field ([#13119](https://github.com/frappe/frappe/pull/13119))
|
||||
- Typo in RecorderDetail.vue ([#13086](https://github.com/frappe/frappe/pull/13086))
|
||||
- Error for bench drop-site. Added missing import. ([#13064](https://github.com/frappe/frappe/pull/13064))
|
||||
- Report column context ([#13090](https://github.com/frappe/frappe/pull/13090))
|
||||
- Different service name for push and pull request events ([#13094](https://github.com/frappe/frappe/pull/13094))
|
||||
- Moving Site folder across different FileSystems failed ([#13033](https://github.com/frappe/frappe/pull/13033))
|
||||
- Consistent checkboxes on all browsers ([#13042](https://github.com/frappe/frappe/pull/13042))
|
||||
- Changed shorcut widgets color picker to dropdown ([#13073](https://github.com/frappe/frappe/pull/13073))
|
||||
- Error while exporting reports with duration field ([#13118](https://github.com/frappe/frappe/pull/13118))
|
||||
- Add margin to download backup card ([#13079](https://github.com/frappe/frappe/pull/13079))
|
||||
- Move mention list generation logic to server-side ([#13074](https://github.com/frappe/frappe/pull/13074))
|
||||
- Make strings translatable ([#13046](https://github.com/frappe/frappe/pull/13046))
|
||||
- Don't evaluate dynamic properties to check if conflicts exist ([#13186](https://github.com/frappe/frappe/pull/13186))
|
||||
- Add __ function in vue global for translation in recorder ([#13089](https://github.com/frappe/frappe/pull/13089))
|
||||
- Make strings translatable ([#13076](https://github.com/frappe/frappe/pull/13076))
|
||||
- Show config in bench CLI ([#13128](https://github.com/frappe/frappe/pull/13128))
|
||||
- Add breadcrumbs for list view ([#13091](https://github.com/frappe/frappe/pull/13091))
|
||||
- Do not skip data in save while using shortcut ([#13182](https://github.com/frappe/frappe/pull/13182))
|
||||
- Use docfields from options if no docfields are returned from meta ([#13188](https://github.com/frappe/frappe/pull/13188))
|
||||
- Disable reloading files in `__pycache__` directory ([#13109](https://github.com/frappe/frappe/pull/13109))
|
||||
- RTL stylesheet route to load RTL style on demand. ([#13007](https://github.com/frappe/frappe/pull/13007))
|
||||
- Do not show messsage when exception is handled ([#13111](https://github.com/frappe/frappe/pull/13111))
|
||||
- Replace parseFloat by Number ([#13082](https://github.com/frappe/frappe/pull/13082))
|
||||
- Add margin to download backup card ([#13050](https://github.com/frappe/frappe/pull/13050))
|
||||
- Translate report column labels ([#13083](https://github.com/frappe/frappe/pull/13083))
|
||||
- Grid row color picker field not working ([#13040](https://github.com/frappe/frappe/pull/13040))
|
||||
- Improve oauthlib implementation ([#13045](https://github.com/frappe/frappe/pull/13045))
|
||||
- Replace filter_by like with full text filter ([#13126](https://github.com/frappe/frappe/pull/13126))
|
||||
- Focus jumps to first field ([#13067](https://github.com/frappe/frappe/pull/13067))
|
||||
|
|
@ -203,10 +203,13 @@ def install_app(context, apps):
|
|||
|
||||
|
||||
@click.command("list-apps")
|
||||
@click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text")
|
||||
@pass_context
|
||||
def list_apps(context):
|
||||
def list_apps(context, format):
|
||||
"List apps in site"
|
||||
|
||||
summary_dict = {}
|
||||
|
||||
def fix_whitespaces(text):
|
||||
if site == context.sites[-1]:
|
||||
text = text.rstrip()
|
||||
|
|
@ -235,18 +238,23 @@ def list_apps(context):
|
|||
]
|
||||
applications_summary = "\n".join(installed_applications)
|
||||
summary = f"{site_title}\n{applications_summary}\n"
|
||||
summary_dict[site] = [app.app_name for app in apps]
|
||||
|
||||
else:
|
||||
applications_summary = "\n".join(frappe.get_installed_apps())
|
||||
installed_applications = frappe.get_installed_apps()
|
||||
applications_summary = "\n".join(installed_applications)
|
||||
summary = f"{site_title}\n{applications_summary}\n"
|
||||
summary_dict[site] = installed_applications
|
||||
|
||||
summary = fix_whitespaces(summary)
|
||||
|
||||
if applications_summary and summary:
|
||||
if format == "text" and applications_summary and summary:
|
||||
print(summary)
|
||||
|
||||
frappe.destroy()
|
||||
|
||||
if format == "json":
|
||||
click.echo(frappe.as_json(summary_dict))
|
||||
|
||||
@click.command('add-system-manager')
|
||||
@click.argument('email')
|
||||
|
|
@ -548,7 +556,7 @@ def move(dest_dir, site):
|
|||
site_dump_exists = os.path.exists(final_new_path)
|
||||
count = int(count or 0) + 1
|
||||
|
||||
os.rename(old_path, final_new_path)
|
||||
shutil.move(old_path, final_new_path)
|
||||
frappe.destroy()
|
||||
return final_new_path
|
||||
|
||||
|
|
|
|||
|
|
@ -96,22 +96,54 @@ def destroy_all_sessions(context, reason=None):
|
|||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('show-config')
|
||||
@click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text")
|
||||
@pass_context
|
||||
def show_config(context):
|
||||
"print configuration file"
|
||||
print("\t\033[92m{:<50}\033[0m \033[92m{:<15}\033[0m".format('Config','Value'))
|
||||
sites_path = os.path.join(frappe.utils.get_bench_path(), 'sites')
|
||||
site_path = context.sites[0]
|
||||
configuration = frappe.get_site_config(sites_path=sites_path, site_path=site_path)
|
||||
print_config(configuration)
|
||||
def show_config(context, format):
|
||||
"Print configuration file to STDOUT in speified format"
|
||||
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
def print_config(config):
|
||||
for conf, value in config.items():
|
||||
if isinstance(value, dict):
|
||||
print_config(value)
|
||||
else:
|
||||
print("\t{:<50} {:<15}".format(conf, value))
|
||||
sites_config = {}
|
||||
sites_path = os.getcwd()
|
||||
|
||||
from frappe.utils.commands import render_table
|
||||
|
||||
def transform_config(config, prefix=None):
|
||||
prefix = f"{prefix}." if prefix else ""
|
||||
site_config = []
|
||||
|
||||
for conf, value in config.items():
|
||||
if isinstance(value, dict):
|
||||
site_config += transform_config(value, prefix=f"{prefix}{conf}")
|
||||
else:
|
||||
log_value = json.dumps(value) if isinstance(value, list) else value
|
||||
site_config += [[f"{prefix}{conf}", log_value]]
|
||||
|
||||
return site_config
|
||||
|
||||
for site in context.sites:
|
||||
frappe.init(site)
|
||||
|
||||
if len(context.sites) != 1 and format == "text":
|
||||
if context.sites.index(site) != 0:
|
||||
click.echo()
|
||||
click.secho(f"Site {site}", fg="yellow")
|
||||
|
||||
configuration = frappe.get_site_config(sites_path=sites_path, site_path=site)
|
||||
|
||||
if format == "text":
|
||||
data = transform_config(configuration)
|
||||
data.insert(0, ['Config','Value'])
|
||||
render_table(data)
|
||||
|
||||
if format == "json":
|
||||
sites_config[site] = configuration
|
||||
|
||||
frappe.destroy()
|
||||
|
||||
if format == "json":
|
||||
click.echo(frappe.as_json(sites_config))
|
||||
|
||||
|
||||
@click.command('reset-perms')
|
||||
|
|
@ -470,6 +502,7 @@ def console(context):
|
|||
locals()[app] = __import__(app)
|
||||
except ModuleNotFoundError:
|
||||
failed_to_import.append(app)
|
||||
all_apps.remove(app)
|
||||
|
||||
print("Apps in this namespace:\n{}".format(", ".join(all_apps)))
|
||||
if failed_to_import:
|
||||
|
|
@ -652,20 +685,27 @@ def make_app(destination, app_name):
|
|||
@click.command('set-config')
|
||||
@click.argument('key')
|
||||
@click.argument('value')
|
||||
@click.option('-g', '--global', 'global_', is_flag = True, default = False, help = 'Set Global Site Config')
|
||||
@click.option('--as-dict', is_flag=True, default=False)
|
||||
@click.option('-g', '--global', 'global_', is_flag=True, default=False, help='Set value in bench config')
|
||||
@click.option('-p', '--parse', is_flag=True, default=False, help='Evaluate as Python Object')
|
||||
@click.option('--as-dict', is_flag=True, default=False, help='Legacy: Evaluate as Python Object')
|
||||
@pass_context
|
||||
def set_config(context, key, value, global_ = False, as_dict=False):
|
||||
def set_config(context, key, value, global_=False, parse=False, as_dict=False):
|
||||
"Insert/Update a value in site_config.json"
|
||||
from frappe.installer import update_site_config
|
||||
import ast
|
||||
|
||||
if as_dict:
|
||||
from frappe.utils.commands import warn
|
||||
warn("--as-dict will be deprecated in v14. Use --parse instead", category=PendingDeprecationWarning)
|
||||
parse = as_dict
|
||||
|
||||
if parse:
|
||||
import ast
|
||||
value = ast.literal_eval(value)
|
||||
|
||||
if global_:
|
||||
sites_path = os.getcwd() # big assumption.
|
||||
sites_path = os.getcwd()
|
||||
common_site_config_path = os.path.join(sites_path, 'common_site_config.json')
|
||||
update_site_config(key, value, validate = False, site_config_path = common_site_config_path)
|
||||
update_site_config(key, value, validate=False, site_config_path=common_site_config_path)
|
||||
else:
|
||||
for site in context.sites:
|
||||
frappe.init(site=site)
|
||||
|
|
@ -722,50 +762,6 @@ def rebuild_global_search(context, static_pages=False):
|
|||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
@click.command('auto-deploy')
|
||||
@click.argument('app')
|
||||
@click.option('--migrate', is_flag=True, default=False, help='Migrate after pulling')
|
||||
@click.option('--restart', is_flag=True, default=False, help='Restart after migration')
|
||||
@click.option('--remote', default='upstream', help='Remote, default is "upstream"')
|
||||
@pass_context
|
||||
def auto_deploy(context, app, migrate=False, restart=False, remote='upstream'):
|
||||
'''Pull and migrate sites that have new version'''
|
||||
from frappe.utils.gitutils import get_app_branch
|
||||
from frappe.utils import get_sites
|
||||
|
||||
branch = get_app_branch(app)
|
||||
app_path = frappe.get_app_path(app)
|
||||
|
||||
# fetch
|
||||
subprocess.check_output(['git', 'fetch', remote, branch], cwd = app_path)
|
||||
|
||||
# get diff
|
||||
if subprocess.check_output(['git', 'diff', '{0}..{1}/{0}'.format(branch, remote)], cwd = app_path):
|
||||
print('Updates found for {0}'.format(app))
|
||||
if app=='frappe':
|
||||
# run bench update
|
||||
import shlex
|
||||
subprocess.check_output(shlex.split('bench update --no-backup'), cwd = '..')
|
||||
else:
|
||||
updated = False
|
||||
subprocess.check_output(['git', 'pull', '--rebase', remote, branch],
|
||||
cwd = app_path)
|
||||
# find all sites with that app
|
||||
for site in get_sites():
|
||||
frappe.init(site)
|
||||
if app in frappe.get_installed_apps():
|
||||
print('Updating {0}'.format(site))
|
||||
updated = True
|
||||
subprocess.check_output(['bench', '--site', site, 'clear-cache'], cwd = '..')
|
||||
if migrate:
|
||||
subprocess.check_output(['bench', '--site', site, 'migrate'], cwd = '..')
|
||||
frappe.destroy()
|
||||
|
||||
if updated or restart:
|
||||
subprocess.check_output(['bench', 'restart'], cwd = '..')
|
||||
else:
|
||||
print('No Updates')
|
||||
|
||||
|
||||
commands = [
|
||||
build,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) {year}, {app_publisher} and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
{base_class_import}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) {year}, {app_publisher} and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
|
|
|||
|
|
@ -83,12 +83,61 @@ class DocType(Document):
|
|||
if not self.is_new():
|
||||
self.before_update = frappe.get_doc('DocType', self.name)
|
||||
self.setup_fields_to_fetch()
|
||||
self.validate_field_name_conflicts()
|
||||
|
||||
check_email_append_to(self)
|
||||
|
||||
if self.default_print_format and not self.custom:
|
||||
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
|
||||
|
||||
if frappe.conf.get('developer_mode'):
|
||||
self.owner = 'Administrator'
|
||||
self.modified_by = 'Administrator'
|
||||
|
||||
def validate_field_name_conflicts(self):
|
||||
"""Check if field names dont conflict with controller properties and methods"""
|
||||
core_doctypes = [
|
||||
"Custom DocPerm",
|
||||
"DocPerm",
|
||||
"Custom Field",
|
||||
"Customize Form Field",
|
||||
"DocField",
|
||||
]
|
||||
|
||||
if self.name in core_doctypes:
|
||||
return
|
||||
|
||||
from frappe.model.base_document import get_controller
|
||||
|
||||
try:
|
||||
controller = get_controller(self.name)
|
||||
except ImportError:
|
||||
controller = Document
|
||||
|
||||
available_objects = {x for x in dir(controller) if isinstance(x, str)}
|
||||
property_set = {
|
||||
x for x in available_objects if isinstance(getattr(controller, x, None), property)
|
||||
}
|
||||
method_set = {
|
||||
x for x in available_objects if x not in property_set and callable(getattr(controller, x, None))
|
||||
}
|
||||
|
||||
for docfield in self.get("fields") or []:
|
||||
conflict_type = None
|
||||
field = docfield.fieldname
|
||||
field_label = docfield.label or docfield.fieldname
|
||||
|
||||
if docfield.fieldname in method_set:
|
||||
conflict_type = "controller method"
|
||||
if docfield.fieldname in property_set:
|
||||
conflict_type = "class property"
|
||||
|
||||
if conflict_type:
|
||||
frappe.throw(
|
||||
_("Fieldname '{0}' conflicting with a {1} of the name {2} in {3}")
|
||||
.format(field_label, conflict_type, field, self.name)
|
||||
)
|
||||
|
||||
def after_insert(self):
|
||||
# clear user cache so that on the next reload this doctype is included in boot
|
||||
clear_user_cache(frappe.session.user)
|
||||
|
|
@ -1174,11 +1223,19 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
|
|||
else:
|
||||
raise
|
||||
|
||||
def check_if_fieldname_conflicts_with_methods(doctype, fieldname):
|
||||
doc = frappe.get_doc({"doctype": doctype})
|
||||
method_list = [method for method in dir(doc) if isinstance(method, str) and callable(getattr(doc, method))]
|
||||
def check_fieldname_conflicts(doctype, fieldname):
|
||||
"""Checks if fieldname conflicts with methods or properties"""
|
||||
|
||||
if fieldname in method_list:
|
||||
doc = frappe.get_doc({"doctype": doctype})
|
||||
available_objects = [x for x in dir(doc) if isinstance(x, str)]
|
||||
property_list = [
|
||||
x for x in available_objects if isinstance(getattr(type(doc), x, None), property)
|
||||
]
|
||||
method_list = [
|
||||
x for x in available_objects if x not in property_list and callable(getattr(doc, x))
|
||||
]
|
||||
|
||||
if fieldname in method_list + property_list:
|
||||
frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname))
|
||||
|
||||
def clear_linked_doctype_cache():
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# Copyright (c) 2013, {app_publisher} and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
|
||||
def execute(filters=None):
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ class User(Document):
|
|||
|
||||
def after_insert(self):
|
||||
create_notification_settings(self.name)
|
||||
frappe.cache().delete_key('users_for_mentions')
|
||||
|
||||
def validate(self):
|
||||
self.check_demo()
|
||||
|
|
@ -129,6 +130,9 @@ class User(Document):
|
|||
if self.time_zone:
|
||||
frappe.defaults.set_default("time_zone", self.time_zone, self.name)
|
||||
|
||||
if self.has_value_changed('allow_in_mentions') or self.has_value_changed('user_type'):
|
||||
frappe.cache().delete_key('users_for_mentions')
|
||||
|
||||
def has_website_permission(self, ptype, user, verbose=False):
|
||||
"""Returns true if current user is the session user"""
|
||||
return self.name == frappe.session.user
|
||||
|
|
@ -389,6 +393,9 @@ class User(Document):
|
|||
# delete notification settings
|
||||
frappe.delete_doc("Notification Settings", self.name, ignore_permissions=True)
|
||||
|
||||
if self.get('allow_in_mentions'):
|
||||
frappe.cache().delete_key('users_for_mentions')
|
||||
|
||||
|
||||
def before_rename(self, old_name, new_name, merge=False):
|
||||
self.check_demo()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import frappe
|
|||
|
||||
class UserGroup(Document):
|
||||
def after_insert(self):
|
||||
frappe.publish_realtime('user_group_added', self.name)
|
||||
frappe.cache().delete_key('user_groups')
|
||||
|
||||
def on_trash(self):
|
||||
frappe.publish_realtime('user_group_deleted', self.name)
|
||||
frappe.cache().delete_key('user_groups')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
frappe.pages['recorder'].on_page_load = function(wrapper) {
|
||||
frappe.ui.make_app_page({
|
||||
parent: wrapper,
|
||||
title: 'Recorder',
|
||||
title: __('Recorder'),
|
||||
single_column: true,
|
||||
card_layout: true
|
||||
});
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ class CustomField(Document):
|
|||
self.translatable = 0
|
||||
|
||||
if not self.flags.ignore_validate:
|
||||
from frappe.core.doctype.doctype.doctype import check_if_fieldname_conflicts_with_methods
|
||||
check_if_fieldname_conflicts_with_methods(self.dt, self.fieldname)
|
||||
from frappe.core.doctype.doctype.doctype import check_fieldname_conflicts
|
||||
check_fieldname_conflicts(self.dt, self.fieldname)
|
||||
|
||||
def on_update(self):
|
||||
if not frappe.flags.in_setup_wizard:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
.download-backup-card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
margin-bottom: var(--margin-lg);
|
||||
}
|
||||
|
||||
.download-backup-card:hover {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
frappe.pages['backups'].on_page_load = function(wrapper) {
|
||||
var page = frappe.ui.make_app_page({
|
||||
parent: wrapper,
|
||||
title: 'Download Backups',
|
||||
title: __('Download Backups'),
|
||||
single_column: true
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
frappe.pages['translation-tool'].on_page_load = function(wrapper) {
|
||||
var page = frappe.ui.make_app_page({
|
||||
parent: wrapper,
|
||||
title: 'Translation Tool',
|
||||
title: __('Translation Tool'),
|
||||
single_column: true,
|
||||
card_layout: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
<div class="chart-wrapper performance-heatmap">
|
||||
<div class="null-state">
|
||||
<span>No Data to Show</span>
|
||||
<span>{%=__("No Data to Show") %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
</div>
|
||||
<div class="chart-wrapper performance-percentage-chart">
|
||||
<div class="null-state">
|
||||
<span>No Data to Show</span>
|
||||
<span>{%=__("No Data to Show") %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
</div>
|
||||
<div class="chart-wrapper performance-line-chart">
|
||||
<div class="null-state">
|
||||
<span>No Data to Show</span>
|
||||
<span>{%=__("No Data to Show") %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -41,4 +41,4 @@
|
|||
<div class="recent-activity-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -377,10 +377,17 @@ def handle_duration_fieldtype_values(result, columns):
|
|||
|
||||
if fieldtype == "Duration":
|
||||
for entry in range(0, len(result)):
|
||||
val_in_seconds = result[entry][i]
|
||||
if val_in_seconds:
|
||||
duration_val = format_duration(val_in_seconds)
|
||||
result[entry][i] = duration_val
|
||||
row = result[entry]
|
||||
if isinstance(row, dict):
|
||||
val_in_seconds = row[col.fieldname]
|
||||
if val_in_seconds:
|
||||
duration_val = format_duration(val_in_seconds)
|
||||
row[col.fieldname] = duration_val
|
||||
else:
|
||||
val_in_seconds = row[i]
|
||||
if val_in_seconds:
|
||||
duration_val = format_duration(val_in_seconds)
|
||||
row[i] = duration_val
|
||||
|
||||
return result
|
||||
|
||||
|
|
|
|||
|
|
@ -221,3 +221,37 @@ def validate_and_sanitize_search_inputs(fn, instance, args, kwargs):
|
|||
return []
|
||||
|
||||
return fn(**kwargs)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_names_for_mentions(search_term):
|
||||
users_for_mentions = frappe.cache().get_value('users_for_mentions', get_users_for_mentions)
|
||||
user_groups = frappe.cache().get_value('user_groups', get_user_groups)
|
||||
|
||||
filtered_mentions = []
|
||||
for mention_data in users_for_mentions + user_groups:
|
||||
if search_term.lower() not in mention_data.value.lower():
|
||||
continue
|
||||
|
||||
mention_data['link'] = frappe.utils.get_url_to_form(
|
||||
'User Group' if mention_data.get('is_group') else 'User Profile',
|
||||
mention_data['id']
|
||||
)
|
||||
|
||||
filtered_mentions.append(mention_data)
|
||||
|
||||
return sorted(filtered_mentions, key=lambda d: d['value'])
|
||||
|
||||
def get_users_for_mentions():
|
||||
return frappe.get_all('User',
|
||||
fields=['name as id', 'full_name as value'],
|
||||
filters={
|
||||
'name': ['not in', ('Administrator', 'Guest')],
|
||||
'allowed_in_mentions': True,
|
||||
'user_type': 'System User',
|
||||
})
|
||||
|
||||
def get_user_groups():
|
||||
return frappe.get_all('User Group', fields=['name as id', 'name as value'], update={
|
||||
'is_group': True
|
||||
})
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ from frappe.utils.background_jobs import get_jobs
|
|||
from frappe.utils.data import get_url, get_link_to_form
|
||||
from frappe.utils.password import get_decrypted_password
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
from frappe.integrations.oauth2 import validate_url
|
||||
|
||||
|
||||
class EventProducer(Document):
|
||||
|
|
@ -56,7 +55,7 @@ class EventProducer(Document):
|
|||
self.reload()
|
||||
|
||||
def check_url(self):
|
||||
if not validate_url(self.producer_url):
|
||||
if not frappe.utils.validate_url(self.producer_url):
|
||||
frappe.throw(_('Invalid URL'))
|
||||
|
||||
# remove '/' from the end of the url like http://test_site.com/
|
||||
|
|
|
|||
|
|
@ -1,256 +1,112 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "field:authorization_code",
|
||||
"beta": 0,
|
||||
"creation": "2016-08-24 14:12:13.647159",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"autoname": "field:authorization_code",
|
||||
"creation": "2016-08-24 14:12:13.647159",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"client",
|
||||
"user",
|
||||
"scopes",
|
||||
"authorization_code",
|
||||
"expiration_time",
|
||||
"redirect_uri_bound_to_authorization_code",
|
||||
"validity",
|
||||
"nonce",
|
||||
"code_challenge",
|
||||
"code_challenge_method"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "client",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Client",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "OAuth Client",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "client",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Client",
|
||||
"options": "OAuth Client",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "User",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "User",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "scopes",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Scopes",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "scopes",
|
||||
"fieldtype": "Text",
|
||||
"label": "Scopes",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "authorization_code",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Authorization Code",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "authorization_code",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Authorization Code",
|
||||
"read_only": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "expiration_time",
|
||||
"fieldtype": "Datetime",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Expiration time",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "expiration_time",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Expiration time",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "redirect_uri_bound_to_authorization_code",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Redirect URI Bound To Auth Code",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "redirect_uri_bound_to_authorization_code",
|
||||
"fieldtype": "Data",
|
||||
"label": "Redirect URI Bound To Auth Code",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "validity",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Validity",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Valid\nInvalid",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "validity",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Validity",
|
||||
"options": "Valid\nInvalid",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "nonce",
|
||||
"fieldtype": "Data",
|
||||
"label": "nonce",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "code_challenge",
|
||||
"fieldtype": "Data",
|
||||
"label": "Code Challenge",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "code_challenge_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Code challenge method",
|
||||
"options": "\ns256\nplain",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-03-08 14:40:04.113884",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "OAuth Authorization Code",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-04-26 07:23:02.980612",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "OAuth Authorization Code",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 0,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"is_custom": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 0
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
|
|
@ -1,283 +1,96 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "field:access_token",
|
||||
"beta": 0,
|
||||
"creation": "2016-08-24 14:10:17.471264",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"autoname": "field:access_token",
|
||||
"creation": "2016-08-24 14:10:17.471264",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"client",
|
||||
"user",
|
||||
"scopes",
|
||||
"access_token",
|
||||
"refresh_token",
|
||||
"expiration_time",
|
||||
"expires_in",
|
||||
"status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "client",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Client",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "OAuth Client",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "client",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Client",
|
||||
"options": "OAuth Client",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "User",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "User",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "scopes",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Scopes",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "scopes",
|
||||
"fieldtype": "Text",
|
||||
"label": "Scopes",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "access_token",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Access Token",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "access_token",
|
||||
"fieldtype": "Data",
|
||||
"label": "Access Token",
|
||||
"read_only": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "refresh_token",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Refresh Token",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "refresh_token",
|
||||
"fieldtype": "Data",
|
||||
"label": "Refresh Token",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "expiration_time",
|
||||
"fieldtype": "Datetime",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Expiration time",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "expiration_time",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Expiration time",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "expires_in",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Expires In",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "expires_in",
|
||||
"fieldtype": "Int",
|
||||
"label": "Expires In",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Active\nRevoked",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "Active\nRevoked",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-03-08 14:40:04.209039",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "OAuth Bearer Token",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-04-26 06:40:34.922441",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "OAuth Bearer Token",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 0,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"is_custom": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 0
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
|
|
@ -1,195 +1,38 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from urllib.parse import quote, urlencode, urlparse
|
||||
|
||||
import jwt
|
||||
from urllib.parse import quote, urlencode
|
||||
from oauthlib.oauth2 import FatalClientError, OAuth2Error
|
||||
from oauthlib.openid.connect.core.endpoints.pre_configured import (
|
||||
Server as WebApplicationServer,
|
||||
)
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.oauth import OAuthWebRequestValidator, WebApplicationServer
|
||||
from frappe.integrations.doctype.oauth_provider_settings.oauth_provider_settings import get_oauth_settings
|
||||
from frappe.oauth import (
|
||||
OAuthWebRequestValidator,
|
||||
generate_json_error_response,
|
||||
get_server_url,
|
||||
get_userinfo,
|
||||
)
|
||||
from frappe.integrations.doctype.oauth_provider_settings.oauth_provider_settings import (
|
||||
get_oauth_settings,
|
||||
)
|
||||
|
||||
|
||||
def get_oauth_server():
|
||||
if not getattr(frappe.local, 'oauth_server', None):
|
||||
if not getattr(frappe.local, "oauth_server", None):
|
||||
oauth_validator = OAuthWebRequestValidator()
|
||||
frappe.local.oauth_server = WebApplicationServer(oauth_validator)
|
||||
|
||||
return frappe.local.oauth_server
|
||||
|
||||
|
||||
def sanitize_kwargs(param_kwargs):
|
||||
"""Remove 'data' and 'cmd' keys, if present."""
|
||||
arguments = param_kwargs
|
||||
arguments.pop('data', None)
|
||||
arguments.pop('cmd', None)
|
||||
arguments.pop("data", None)
|
||||
arguments.pop("cmd", None)
|
||||
|
||||
return arguments
|
||||
|
||||
@frappe.whitelist()
|
||||
def approve(*args, **kwargs):
|
||||
r = frappe.request
|
||||
|
||||
try:
|
||||
scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request(
|
||||
r.url,
|
||||
r.method,
|
||||
r.get_data(),
|
||||
r.headers
|
||||
)
|
||||
|
||||
headers, body, status = get_oauth_server().create_authorization_response(
|
||||
uri=frappe.flags.oauth_credentials['redirect_uri'],
|
||||
body=r.get_data(),
|
||||
headers=r.headers,
|
||||
scopes=scopes,
|
||||
credentials=frappe.flags.oauth_credentials
|
||||
)
|
||||
uri = headers.get('Location', None)
|
||||
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = uri
|
||||
|
||||
except FatalClientError as e:
|
||||
return e
|
||||
except OAuth2Error as e:
|
||||
return e
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def authorize(**kwargs):
|
||||
success_url = "/api/method/frappe.integrations.oauth2.approve?" + encode_params(sanitize_kwargs(kwargs))
|
||||
failure_url = frappe.form_dict["redirect_uri"] + "?error=access_denied"
|
||||
|
||||
if frappe.session.user == 'Guest':
|
||||
#Force login, redirect to preauth again.
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = "/login?" + encode_params({'redirect-to': frappe.request.url})
|
||||
else:
|
||||
try:
|
||||
r = frappe.request
|
||||
scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request(
|
||||
r.url,
|
||||
r.method,
|
||||
r.get_data(),
|
||||
r.headers
|
||||
)
|
||||
|
||||
skip_auth = frappe.db.get_value("OAuth Client", frappe.flags.oauth_credentials['client_id'], "skip_authorization")
|
||||
unrevoked_tokens = frappe.get_all("OAuth Bearer Token", filters={"status":"Active"})
|
||||
|
||||
if skip_auth or (get_oauth_settings().skip_authorization == "Auto" and unrevoked_tokens):
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = success_url
|
||||
else:
|
||||
#Show Allow/Deny screen.
|
||||
response_html_params = frappe._dict({
|
||||
"client_id": frappe.db.get_value("OAuth Client", kwargs['client_id'], "app_name"),
|
||||
"success_url": success_url,
|
||||
"failure_url": failure_url,
|
||||
"details": scopes
|
||||
})
|
||||
resp_html = frappe.render_template("templates/includes/oauth_confirmation.html", response_html_params)
|
||||
frappe.respond_as_web_page("Confirm Access", resp_html)
|
||||
except FatalClientError as e:
|
||||
return e
|
||||
except OAuth2Error as e:
|
||||
return e
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_token(*args, **kwargs):
|
||||
#Check whether frappe server URL is set
|
||||
frappe_server_url = frappe.db.get_value("Social Login Key", "frappe", "base_url") or None
|
||||
if not frappe_server_url:
|
||||
frappe.throw(_("Please set Base URL in Social Login Key for Frappe"))
|
||||
|
||||
try:
|
||||
r = frappe.request
|
||||
headers, body, status = get_oauth_server().create_token_response(
|
||||
r.url,
|
||||
r.method,
|
||||
r.form,
|
||||
r.headers,
|
||||
frappe.flags.oauth_credentials
|
||||
)
|
||||
out = frappe._dict(json.loads(body))
|
||||
if not out.error and "openid" in out.scope:
|
||||
token_user = frappe.db.get_value("OAuth Bearer Token", out.access_token, "user")
|
||||
token_client = frappe.db.get_value("OAuth Bearer Token", out.access_token, "client")
|
||||
client_secret = frappe.db.get_value("OAuth Client", token_client, "client_secret")
|
||||
if token_user in ["Guest", "Administrator"]:
|
||||
frappe.throw(_("Logged in as Guest or Administrator"))
|
||||
|
||||
id_token_header = {
|
||||
"typ":"jwt",
|
||||
"alg":"HS256"
|
||||
}
|
||||
id_token = {
|
||||
"aud": token_client,
|
||||
"exp": int((frappe.db.get_value("OAuth Bearer Token", out.access_token, "expiration_time") - frappe.utils.datetime.datetime(1970, 1, 1)).total_seconds()),
|
||||
"sub": frappe.db.get_value("User Social Login", {"parent":token_user, "provider": "frappe"}, "userid"),
|
||||
"iss": frappe_server_url,
|
||||
"at_hash": frappe.oauth.calculate_at_hash(out.access_token, hashlib.sha256)
|
||||
}
|
||||
|
||||
id_token_encoded = jwt.encode(id_token, client_secret, algorithm='HS256', headers=id_token_header)
|
||||
out.update({"id_token": frappe.safe_decode(id_token_encoded)})
|
||||
|
||||
frappe.local.response = out
|
||||
|
||||
except FatalClientError as e:
|
||||
return e
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def revoke_token(*args, **kwargs):
|
||||
r = frappe.request
|
||||
headers, body, status = get_oauth_server().create_revocation_response(
|
||||
r.url,
|
||||
headers=r.headers,
|
||||
body=r.form,
|
||||
http_method=r.method
|
||||
)
|
||||
|
||||
frappe.local.response['http_status_code'] = status
|
||||
if status == 200:
|
||||
return "success"
|
||||
else:
|
||||
return "bad request"
|
||||
|
||||
@frappe.whitelist()
|
||||
def openid_profile(*args, **kwargs):
|
||||
picture = None
|
||||
first_name, last_name, avatar, name = frappe.db.get_value("User", frappe.session.user, ["first_name", "last_name", "user_image", "name"])
|
||||
frappe_userid = frappe.db.get_value("User Social Login", {"parent":frappe.session.user, "provider": "frappe"}, "userid")
|
||||
request_url = urlparse(frappe.request.url)
|
||||
base_url = frappe.db.get_value("Social Login Key", "frappe", "base_url") or None
|
||||
|
||||
if avatar:
|
||||
if validate_url(avatar):
|
||||
picture = avatar
|
||||
elif base_url:
|
||||
picture = base_url + '/' + avatar
|
||||
else:
|
||||
picture = request_url.scheme + "://" + request_url.netloc + avatar
|
||||
|
||||
user_profile = frappe._dict({
|
||||
"sub": frappe_userid,
|
||||
"name": " ".join(filter(None, [first_name, last_name])),
|
||||
"given_name": first_name,
|
||||
"family_name": last_name,
|
||||
"email": name,
|
||||
"picture": picture
|
||||
})
|
||||
|
||||
frappe.local.response = user_profile
|
||||
|
||||
def validate_url(url_string):
|
||||
try:
|
||||
result = urlparse(url_string)
|
||||
return result.scheme and result.scheme in ["http", "https", "ftp", "ftps"]
|
||||
except:
|
||||
return False
|
||||
|
||||
def encode_params(params):
|
||||
"""
|
||||
|
|
@ -200,3 +43,215 @@ def encode_params(params):
|
|||
as a whitespace.
|
||||
"""
|
||||
return urlencode(params, quote_via=quote)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def approve(*args, **kwargs):
|
||||
r = frappe.request
|
||||
|
||||
try:
|
||||
(
|
||||
scopes,
|
||||
frappe.flags.oauth_credentials,
|
||||
) = get_oauth_server().validate_authorization_request(
|
||||
r.url, r.method, r.get_data(), r.headers
|
||||
)
|
||||
|
||||
headers, body, status = get_oauth_server().create_authorization_response(
|
||||
uri=frappe.flags.oauth_credentials["redirect_uri"],
|
||||
body=r.get_data(),
|
||||
headers=r.headers,
|
||||
scopes=scopes,
|
||||
credentials=frappe.flags.oauth_credentials,
|
||||
)
|
||||
uri = headers.get("Location", None)
|
||||
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = uri
|
||||
return
|
||||
|
||||
except (FatalClientError, OAuth2Error) as e:
|
||||
return generate_json_error_response(e)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def authorize(**kwargs):
|
||||
success_url = "/api/method/frappe.integrations.oauth2.approve?" + encode_params(
|
||||
sanitize_kwargs(kwargs)
|
||||
)
|
||||
failure_url = frappe.form_dict["redirect_uri"] + "?error=access_denied"
|
||||
|
||||
if frappe.session.user == "Guest":
|
||||
# Force login, redirect to preauth again.
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = "/login?" + encode_params(
|
||||
{"redirect-to": frappe.request.url}
|
||||
)
|
||||
else:
|
||||
try:
|
||||
r = frappe.request
|
||||
(
|
||||
scopes,
|
||||
frappe.flags.oauth_credentials,
|
||||
) = get_oauth_server().validate_authorization_request(
|
||||
r.url, r.method, r.get_data(), r.headers
|
||||
)
|
||||
|
||||
skip_auth = frappe.db.get_value(
|
||||
"OAuth Client",
|
||||
frappe.flags.oauth_credentials["client_id"],
|
||||
"skip_authorization",
|
||||
)
|
||||
unrevoked_tokens = frappe.get_all(
|
||||
"OAuth Bearer Token", filters={"status": "Active"}
|
||||
)
|
||||
|
||||
if skip_auth or (
|
||||
get_oauth_settings().skip_authorization == "Auto" and unrevoked_tokens
|
||||
):
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = success_url
|
||||
else:
|
||||
# Show Allow/Deny screen.
|
||||
response_html_params = frappe._dict(
|
||||
{
|
||||
"client_id": frappe.db.get_value(
|
||||
"OAuth Client", kwargs["client_id"], "app_name"
|
||||
),
|
||||
"success_url": success_url,
|
||||
"failure_url": failure_url,
|
||||
"details": scopes,
|
||||
}
|
||||
)
|
||||
resp_html = frappe.render_template(
|
||||
"templates/includes/oauth_confirmation.html", response_html_params
|
||||
)
|
||||
frappe.respond_as_web_page("Confirm Access", resp_html)
|
||||
except (FatalClientError, OAuth2Error) as e:
|
||||
return generate_json_error_response(e)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_token(*args, **kwargs):
|
||||
try:
|
||||
r = frappe.request
|
||||
headers, body, status = get_oauth_server().create_token_response(
|
||||
r.url, r.method, r.form, r.headers, frappe.flags.oauth_credentials
|
||||
)
|
||||
body = frappe._dict(json.loads(body))
|
||||
|
||||
if body.error:
|
||||
frappe.local.response = body
|
||||
frappe.local.response["http_status_code"] = 400
|
||||
return
|
||||
|
||||
frappe.local.response = body
|
||||
return
|
||||
|
||||
except (FatalClientError, OAuth2Error) as e:
|
||||
return generate_json_error_response(e)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def revoke_token(*args, **kwargs):
|
||||
try:
|
||||
r = frappe.request
|
||||
headers, body, status = get_oauth_server().create_revocation_response(
|
||||
r.url,
|
||||
headers=r.headers,
|
||||
body=r.form,
|
||||
http_method=r.method,
|
||||
)
|
||||
except (FatalClientError, OAuth2Error):
|
||||
pass
|
||||
|
||||
# status_code must be 200
|
||||
frappe.local.response = frappe._dict({})
|
||||
frappe.local.response["http_status_code"] = status or 200
|
||||
return
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def openid_profile(*args, **kwargs):
|
||||
try:
|
||||
r = frappe.request
|
||||
headers, body, status = get_oauth_server().create_userinfo_response(
|
||||
r.url,
|
||||
headers=r.headers,
|
||||
body=r.form,
|
||||
)
|
||||
body = frappe._dict(json.loads(body))
|
||||
frappe.local.response = body
|
||||
return
|
||||
|
||||
except (FatalClientError, OAuth2Error) as e:
|
||||
return generate_json_error_response(e)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def openid_configuration():
|
||||
frappe_server_url = get_server_url()
|
||||
frappe.local.response = frappe._dict(
|
||||
{
|
||||
"issuer": frappe_server_url,
|
||||
"authorization_endpoint": f"{frappe_server_url}/api/method/frappe.integrations.oauth2.authorize",
|
||||
"token_endpoint": f"{frappe_server_url}/api/method/frappe.integrations.oauth2.get_token",
|
||||
"userinfo_endpoint": f"{frappe_server_url}/api/method/frappe.integrations.oauth2.openid_profile",
|
||||
"revocation_endpoint": f"{frappe_server_url}/api/method/frappe.integrations.oauth2.revoke_token",
|
||||
"introspection_endpoint": f"{frappe_server_url}/api/method/frappe.integrations.oauth2.introspect_token",
|
||||
"response_types_supported": [
|
||||
"code",
|
||||
"token",
|
||||
"code id_token",
|
||||
"code token id_token",
|
||||
"id_token",
|
||||
"id_token token",
|
||||
],
|
||||
"subject_types_supported": ["public"],
|
||||
"id_token_signing_alg_values_supported": ["HS256"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def introspect_token(token=None, token_type_hint=None):
|
||||
if token_type_hint not in ["access_token", "refresh_token"]:
|
||||
token_type_hint = "access_token"
|
||||
try:
|
||||
bearer_token = None
|
||||
if token_type_hint == "access_token":
|
||||
bearer_token = frappe.get_doc("OAuth Bearer Token", {"access_token": token})
|
||||
elif token_type_hint == "refresh_token":
|
||||
bearer_token = frappe.get_doc(
|
||||
"OAuth Bearer Token", {"refresh_token": token}
|
||||
)
|
||||
|
||||
client = frappe.get_doc("OAuth Client", bearer_token.client)
|
||||
|
||||
token_response = frappe._dict(
|
||||
{
|
||||
"client_id": client.client_id,
|
||||
"trusted_client": client.skip_authorization,
|
||||
"active": bearer_token.status == "Active",
|
||||
"exp": round(bearer_token.expiration_time.timestamp()),
|
||||
"scope": bearer_token.scopes,
|
||||
}
|
||||
)
|
||||
|
||||
if "openid" in bearer_token.scopes:
|
||||
sub = frappe.get_value(
|
||||
"User Social Login",
|
||||
{"provider": "frappe", "parent": bearer_token.user},
|
||||
"userid",
|
||||
)
|
||||
|
||||
if sub:
|
||||
token_response.update({"sub": sub})
|
||||
user = frappe.get_doc("User", bearer_token.user)
|
||||
userinfo = get_userinfo(user)
|
||||
token_response.update(userinfo)
|
||||
|
||||
frappe.local.response = token_response
|
||||
|
||||
except Exception:
|
||||
frappe.local.response = frappe._dict({"active": False})
|
||||
|
|
|
|||
|
|
@ -34,8 +34,9 @@ def get_controller(doctype):
|
|||
from frappe.model.document import Document
|
||||
from frappe.utils.nestedset import NestedSet
|
||||
|
||||
module_name, custom = frappe.db.get_value("DocType", doctype, ("module", "custom"), cache=True) \
|
||||
or ["Core", False]
|
||||
module_name, custom = frappe.db.get_value(
|
||||
"DocType", doctype, ("module", "custom"), cache=True
|
||||
) or ["Core", False]
|
||||
|
||||
if custom:
|
||||
if frappe.db.field_exists("DocType", "is_tree"):
|
||||
|
|
|
|||
|
|
@ -1347,6 +1347,22 @@ class Document(BaseDocument):
|
|||
from frappe.desk.doctype.tag.tag import DocTags
|
||||
return DocTags(self.doctype).get_tags(self.name).split(",")[1:]
|
||||
|
||||
def __repr__(self):
|
||||
name = self.name or "unsaved"
|
||||
doctype = self.__class__.__name__
|
||||
|
||||
docstatus = f" docstatus={self.docstatus}" if self.docstatus else ""
|
||||
parent = f" parent={self.parent}" if self.parent else ""
|
||||
|
||||
return f"<{doctype}: {name}{docstatus}{parent}>"
|
||||
|
||||
def __str__(self):
|
||||
name = self.name or "unsaved"
|
||||
doctype = self.__class__.__name__
|
||||
|
||||
return f"{doctype}({name})"
|
||||
|
||||
|
||||
def execute_action(doctype, name, action, **kwargs):
|
||||
"""Execute an action on a document (called by background worker)"""
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
|
|
|
|||
550
frappe/oauth.py
550
frappe/oauth.py
|
|
@ -1,65 +1,16 @@
|
|||
from __future__ import print_function, unicode_literals
|
||||
import frappe
|
||||
import pytz
|
||||
import jwt
|
||||
import hashlib
|
||||
import re
|
||||
import base64
|
||||
import datetime
|
||||
|
||||
from frappe import _
|
||||
from frappe.auth import LoginManager
|
||||
from http import cookies
|
||||
from oauthlib.oauth2.rfc6749.tokens import BearerToken
|
||||
from oauthlib.oauth2.rfc6749.grant_types import AuthorizationCodeGrant, ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant, RefreshTokenGrant
|
||||
from oauthlib.oauth2 import RequestValidator
|
||||
from oauthlib.oauth2.rfc6749.endpoints.authorization import AuthorizationEndpoint
|
||||
from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint
|
||||
from oauthlib.oauth2.rfc6749.endpoints.resource import ResourceEndpoint
|
||||
from oauthlib.oauth2.rfc6749.endpoints.revocation import RevocationEndpoint
|
||||
from oauthlib.common import Request
|
||||
from six.moves.urllib.parse import unquote
|
||||
from oauthlib.openid import RequestValidator
|
||||
from urllib.parse import urlparse, unquote
|
||||
|
||||
def get_url_delimiter(separator_character=" "):
|
||||
return separator_character
|
||||
|
||||
class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint,
|
||||
RevocationEndpoint):
|
||||
|
||||
"""An all-in-one endpoint featuring Authorization code grant and Bearer tokens."""
|
||||
|
||||
def __init__(self, request_validator, token_generator=None,
|
||||
token_expires_in=None, refresh_token_generator=None, **kwargs):
|
||||
"""Construct a new web application server.
|
||||
|
||||
:param request_validator: An implementation of
|
||||
oauthlib.oauth2.RequestValidator.
|
||||
:param token_expires_in: An int or a function to generate a token
|
||||
expiration offset (in seconds) given a
|
||||
oauthlib.common.Request object.
|
||||
:param token_generator: A function to generate a token from a request.
|
||||
:param refresh_token_generator: A function to generate a token from a
|
||||
request for the refresh token.
|
||||
:param kwargs: Extra parameters to pass to authorization-,
|
||||
token-, resource-, and revocation-endpoint constructors.
|
||||
"""
|
||||
implicit_grant = ImplicitGrant(request_validator)
|
||||
auth_grant = AuthorizationCodeGrant(request_validator)
|
||||
refresh_grant = RefreshTokenGrant(request_validator)
|
||||
resource_owner_password_credentials_grant = ResourceOwnerPasswordCredentialsGrant(request_validator)
|
||||
bearer = BearerToken(request_validator, token_generator,
|
||||
token_expires_in, refresh_token_generator)
|
||||
AuthorizationEndpoint.__init__(self, default_response_type='code',
|
||||
response_types={
|
||||
'code': auth_grant,
|
||||
'token': implicit_grant
|
||||
},
|
||||
default_token_type=bearer)
|
||||
TokenEndpoint.__init__(self, default_grant_type='authorization_code',
|
||||
grant_types={
|
||||
'authorization_code': auth_grant,
|
||||
'refresh_token': refresh_grant,
|
||||
'password': resource_owner_password_credentials_grant
|
||||
},
|
||||
default_token_type=bearer)
|
||||
ResourceEndpoint.__init__(self, default_token='Bearer',
|
||||
token_types={'Bearer': bearer})
|
||||
RevocationEndpoint.__init__(self, request_validator)
|
||||
import frappe
|
||||
from frappe.auth import LoginManager
|
||||
|
||||
|
||||
class OAuthWebRequestValidator(RequestValidator):
|
||||
|
|
@ -67,7 +18,7 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
# Pre- and post-authorization.
|
||||
def validate_client_id(self, client_id, request, *args, **kwargs):
|
||||
# Simple validity check, does client exist? Not banned?
|
||||
cli_id = frappe.db.get_value("OAuth Client",{ "name":client_id })
|
||||
cli_id = frappe.db.get_value("OAuth Client", {"name": client_id})
|
||||
if cli_id:
|
||||
request.client = frappe.get_doc("OAuth Client", client_id).as_dict()
|
||||
return True
|
||||
|
|
@ -78,7 +29,9 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
# Is the client allowed to use the supplied redirect_uri? i.e. has
|
||||
# the client previously registered this EXACT redirect uri.
|
||||
|
||||
redirect_uris = frappe.db.get_value("OAuth Client", client_id, 'redirect_uris').split(get_url_delimiter())
|
||||
redirect_uris = frappe.db.get_value(
|
||||
"OAuth Client", client_id, "redirect_uris"
|
||||
).split(get_url_delimiter())
|
||||
|
||||
if redirect_uri in redirect_uris:
|
||||
return True
|
||||
|
|
@ -89,7 +42,9 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
# The redirect used if none has been supplied.
|
||||
# Prefer your clients to pre register a redirect uri rather than
|
||||
# supplying one on each authorization request.
|
||||
redirect_uri = frappe.db.get_value("OAuth Client", client_id, 'default_redirect_uri')
|
||||
redirect_uri = frappe.db.get_value(
|
||||
"OAuth Client", client_id, "default_redirect_uri"
|
||||
)
|
||||
return redirect_uri
|
||||
|
||||
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
|
||||
|
|
@ -101,19 +56,23 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
# Scopes a client will authorize for if none are supplied in the
|
||||
# authorization request.
|
||||
scopes = get_client_scopes(client_id)
|
||||
request.scopes = scopes #Apparently this is possible.
|
||||
request.scopes = scopes # Apparently this is possible.
|
||||
return scopes
|
||||
|
||||
def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs):
|
||||
# Clients should only be allowed to use one type of response type, the
|
||||
# one associated with their one allowed grant type.
|
||||
# In this case it must be "code".
|
||||
allowed_response_types = [client.response_type.lower(),
|
||||
"code token", "code id_token", "code token id_token",
|
||||
"code+token", "code+id_token", "code+token id_token"]
|
||||
|
||||
return (response_type in allowed_response_types)
|
||||
def validate_response_type(
|
||||
self, client_id, response_type, client, request, *args, **kwargs
|
||||
):
|
||||
allowed_response_types = [
|
||||
# From OAuth Client response_type field
|
||||
client.response_type.lower(),
|
||||
# OIDC
|
||||
"id_token",
|
||||
"id_token token",
|
||||
"code id_token",
|
||||
"code token id_token",
|
||||
]
|
||||
|
||||
return response_type in allowed_response_types
|
||||
|
||||
# Post-authorization
|
||||
|
||||
|
|
@ -121,38 +80,69 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
|
||||
cookie_dict = get_cookie_dict_from_headers(request)
|
||||
|
||||
oac = frappe.new_doc('OAuth Authorization Code')
|
||||
oac = frappe.new_doc("OAuth Authorization Code")
|
||||
oac.scopes = get_url_delimiter().join(request.scopes)
|
||||
oac.redirect_uri_bound_to_authorization_code = request.redirect_uri
|
||||
oac.client = client_id
|
||||
oac.user = unquote(cookie_dict['user_id'].value)
|
||||
oac.authorization_code = code['code']
|
||||
oac.user = unquote(cookie_dict["user_id"].value)
|
||||
oac.authorization_code = code["code"]
|
||||
|
||||
if request.nonce:
|
||||
oac.nonce = request.nonce
|
||||
|
||||
if request.code_challenge and request.code_challenge_method:
|
||||
oac.code_challenge = request.code_challenge
|
||||
oac.code_challenge_method = request.code_challenge_method.lower()
|
||||
|
||||
oac.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
|
||||
def authenticate_client(self, request, *args, **kwargs):
|
||||
#Get ClientID in URL
|
||||
# Get ClientID in URL
|
||||
if request.client_id:
|
||||
oc = frappe.get_doc("OAuth Client", request.client_id)
|
||||
else:
|
||||
#Extract token, instantiate OAuth Bearer Token and use clientid from there.
|
||||
# Extract token, instantiate OAuth Bearer Token and use clientid from there.
|
||||
if "refresh_token" in frappe.form_dict:
|
||||
oc = frappe.get_doc("OAuth Client", frappe.db.get_value("OAuth Bearer Token", {"refresh_token": frappe.form_dict["refresh_token"]}, 'client'))
|
||||
oc = frappe.get_doc(
|
||||
"OAuth Client",
|
||||
frappe.db.get_value(
|
||||
"OAuth Bearer Token",
|
||||
{"refresh_token": frappe.form_dict["refresh_token"]},
|
||||
"client",
|
||||
),
|
||||
)
|
||||
elif "token" in frappe.form_dict:
|
||||
oc = frappe.get_doc("OAuth Client", frappe.db.get_value("OAuth Bearer Token", frappe.form_dict["token"], 'client'))
|
||||
oc = frappe.get_doc(
|
||||
"OAuth Client",
|
||||
frappe.db.get_value(
|
||||
"OAuth Bearer Token", frappe.form_dict["token"], "client"
|
||||
),
|
||||
)
|
||||
else:
|
||||
oc = frappe.get_doc("OAuth Client", frappe.db.get_value("OAuth Bearer Token", frappe.get_request_header("Authorization").split(" ")[1], 'client'))
|
||||
oc = frappe.get_doc(
|
||||
"OAuth Client",
|
||||
frappe.db.get_value(
|
||||
"OAuth Bearer Token",
|
||||
frappe.get_request_header("Authorization").split(" ")[1],
|
||||
"client",
|
||||
),
|
||||
)
|
||||
try:
|
||||
request.client = request.client or oc.as_dict()
|
||||
except Exception as e:
|
||||
print("Failed body authentication: Application %s does not exist".format(cid=request.client_id))
|
||||
return generate_json_error_response(e)
|
||||
|
||||
cookie_dict = get_cookie_dict_from_headers(request)
|
||||
user_id = unquote(cookie_dict.get('user_id').value) if 'user_id' in cookie_dict else "Guest"
|
||||
user_id = (
|
||||
unquote(cookie_dict.get("user_id").value)
|
||||
if "user_id" in cookie_dict
|
||||
else "Guest"
|
||||
)
|
||||
return frappe.session.user == user_id
|
||||
|
||||
def authenticate_client_id(self, client_id, request, *args, **kwargs):
|
||||
cli_id = frappe.db.get_value('OAuth Client', client_id, 'name')
|
||||
cli_id = frappe.db.get_value("OAuth Client", client_id, "name")
|
||||
if not cli_id:
|
||||
# Don't allow public (non-authenticated) clients
|
||||
return False
|
||||
|
|
@ -164,28 +154,72 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
# Validate the code belongs to the client. Add associated scopes,
|
||||
# state and user to request.scopes and request.user.
|
||||
|
||||
validcodes = frappe.get_all("OAuth Authorization Code", filters={"client": client_id, "validity": "Valid"})
|
||||
validcodes = frappe.get_all(
|
||||
"OAuth Authorization Code",
|
||||
filters={"client": client_id, "validity": "Valid"},
|
||||
)
|
||||
|
||||
checkcodes = []
|
||||
for vcode in validcodes:
|
||||
checkcodes.append(vcode["name"])
|
||||
|
||||
if code in checkcodes:
|
||||
request.scopes = frappe.db.get_value("OAuth Authorization Code", code, 'scopes').split(get_url_delimiter())
|
||||
request.user = frappe.db.get_value("OAuth Authorization Code", code, 'user')
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
request.scopes = frappe.db.get_value(
|
||||
"OAuth Authorization Code", code, "scopes"
|
||||
).split(get_url_delimiter())
|
||||
request.user = frappe.db.get_value("OAuth Authorization Code", code, "user")
|
||||
code_challenge_method = frappe.db.get_value(
|
||||
"OAuth Authorization Code", code, "code_challenge_method"
|
||||
)
|
||||
code_challenge = frappe.db.get_value(
|
||||
"OAuth Authorization Code", code, "code_challenge"
|
||||
)
|
||||
|
||||
def confirm_redirect_uri(self, client_id, code, redirect_uri, client, *args, **kwargs):
|
||||
saved_redirect_uri = frappe.db.get_value('OAuth Client', client_id, 'default_redirect_uri')
|
||||
if code_challenge and not request.code_verifier:
|
||||
if frappe.db.exists("OAuth Authorization Code", code):
|
||||
frappe.delete_doc(
|
||||
"OAuth Authorization Code", code, ignore_permissions=True
|
||||
)
|
||||
frappe.db.commit()
|
||||
return False
|
||||
|
||||
if code_challenge_method == "s256":
|
||||
m = hashlib.sha256()
|
||||
m.update(bytes(request.code_verifier, "utf-8"))
|
||||
code_verifier = base64.b64encode(m.digest()).decode("utf-8")
|
||||
code_verifier = re.sub(r"\+", "-", code_verifier)
|
||||
code_verifier = re.sub(r"\/", "_", code_verifier)
|
||||
code_verifier = re.sub(r"=", "", code_verifier)
|
||||
return code_challenge == code_verifier
|
||||
|
||||
elif code_challenge_method == "plain":
|
||||
return code_challenge == request.code_verifier
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def confirm_redirect_uri(
|
||||
self, client_id, code, redirect_uri, client, *args, **kwargs
|
||||
):
|
||||
saved_redirect_uri = frappe.db.get_value(
|
||||
"OAuth Client", client_id, "default_redirect_uri"
|
||||
)
|
||||
|
||||
redirect_uris = frappe.db.get_value("OAuth Client", client_id, "redirect_uris")
|
||||
|
||||
if redirect_uris:
|
||||
redirect_uris = redirect_uris.split(get_url_delimiter())
|
||||
return redirect_uri in redirect_uris
|
||||
|
||||
return saved_redirect_uri == redirect_uri
|
||||
|
||||
def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs):
|
||||
def validate_grant_type(
|
||||
self, client_id, grant_type, client, request, *args, **kwargs
|
||||
):
|
||||
# Clients should only be allowed to use one type of grant.
|
||||
# In this case, it must be "authorization_code" or "refresh_token"
|
||||
return (grant_type in ["authorization_code", "refresh_token", "password"])
|
||||
return grant_type in ["authorization_code", "refresh_token", "password"]
|
||||
|
||||
def save_bearer_token(self, token, request, *args, **kwargs):
|
||||
# Remember to associate it with request.scopes, request.user and
|
||||
|
|
@ -195,19 +229,30 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
# access_token to now + expires_in seconds.
|
||||
|
||||
otoken = frappe.new_doc("OAuth Bearer Token")
|
||||
otoken.client = request.client['name']
|
||||
otoken.client = request.client["name"]
|
||||
try:
|
||||
otoken.user = request.user if request.user else frappe.db.get_value("OAuth Bearer Token", {"refresh_token":request.body.get("refresh_token")}, "user")
|
||||
except Exception as e:
|
||||
otoken.user = (
|
||||
request.user
|
||||
if request.user
|
||||
else frappe.db.get_value(
|
||||
"OAuth Bearer Token",
|
||||
{"refresh_token": request.body.get("refresh_token")},
|
||||
"user",
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
otoken.user = frappe.session.user
|
||||
|
||||
otoken.scopes = get_url_delimiter().join(request.scopes)
|
||||
otoken.access_token = token['access_token']
|
||||
otoken.refresh_token = token.get('refresh_token')
|
||||
otoken.expires_in = token['expires_in']
|
||||
otoken.access_token = token["access_token"]
|
||||
otoken.refresh_token = token.get("refresh_token")
|
||||
otoken.expires_in = token["expires_in"]
|
||||
otoken.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
|
||||
default_redirect_uri = frappe.db.get_value("OAuth Client", request.client['name'], "default_redirect_uri")
|
||||
default_redirect_uri = frappe.db.get_value(
|
||||
"OAuth Client", request.client["name"], "default_redirect_uri"
|
||||
)
|
||||
return default_redirect_uri
|
||||
|
||||
def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs):
|
||||
|
|
@ -222,24 +267,35 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
def validate_bearer_token(self, token, scopes, request):
|
||||
# Remember to check expiration and scope membership
|
||||
otoken = frappe.get_doc("OAuth Bearer Token", token)
|
||||
token_expiration_local = otoken.expiration_time.replace(tzinfo=pytz.timezone(frappe.utils.get_time_zone()))
|
||||
token_expiration_local = otoken.expiration_time.replace(
|
||||
tzinfo=pytz.timezone(frappe.utils.get_time_zone())
|
||||
)
|
||||
token_expiration_utc = token_expiration_local.astimezone(pytz.utc)
|
||||
is_token_valid = (frappe.utils.datetime.datetime.utcnow().replace(tzinfo=pytz.utc) < token_expiration_utc) \
|
||||
and otoken.status != "Revoked"
|
||||
client_scopes = frappe.db.get_value("OAuth Client", otoken.client, 'scopes').split(get_url_delimiter())
|
||||
is_token_valid = (
|
||||
frappe.utils.datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
|
||||
< token_expiration_utc
|
||||
) and otoken.status != "Revoked"
|
||||
client_scopes = frappe.db.get_value(
|
||||
"OAuth Client", otoken.client, "scopes"
|
||||
).split(get_url_delimiter())
|
||||
are_scopes_valid = True
|
||||
for scp in scopes:
|
||||
are_scopes_valid = are_scopes_valid and True if scp in client_scopes else False
|
||||
are_scopes_valid = (
|
||||
are_scopes_valid and True if scp in client_scopes else False
|
||||
)
|
||||
|
||||
return is_token_valid and are_scopes_valid
|
||||
|
||||
# Token refresh request
|
||||
|
||||
def get_original_scopes(self, refresh_token, request, *args, **kwargs):
|
||||
# Obtain the token associated with the given refresh_token and
|
||||
# return its scopes, these will be passed on to the refreshed
|
||||
# access token if the client did not specify a scope during the
|
||||
# request.
|
||||
obearer_token = frappe.get_doc("OAuth Bearer Token", {"refresh_token": refresh_token})
|
||||
obearer_token = frappe.get_doc(
|
||||
"OAuth Bearer Token", {"refresh_token": refresh_token}
|
||||
)
|
||||
return obearer_token.scopes
|
||||
|
||||
def revoke_token(self, token, token_type_hint, request, *args, **kwargs):
|
||||
|
|
@ -250,36 +306,38 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
:param request: The HTTP Request (oauthlib.common.Request)
|
||||
|
||||
Method is used by:
|
||||
- Revocation Endpoint
|
||||
- Revocation Endpoint
|
||||
"""
|
||||
otoken = None
|
||||
|
||||
if token_type_hint == "access_token":
|
||||
otoken = frappe.db.set_value("OAuth Bearer Token", token, 'status', 'Revoked')
|
||||
frappe.db.set_value("OAuth Bearer Token", token, "status", "Revoked")
|
||||
elif token_type_hint == "refresh_token":
|
||||
otoken = frappe.db.set_value("OAuth Bearer Token", {"refresh_token": token}, 'status', 'Revoked')
|
||||
frappe.db.set_value(
|
||||
"OAuth Bearer Token", {"refresh_token": token}, "status", "Revoked"
|
||||
)
|
||||
else:
|
||||
otoken = frappe.db.set_value("OAuth Bearer Token", token, 'status', 'Revoked')
|
||||
frappe.db.set_value("OAuth Bearer Token", token, "status", "Revoked")
|
||||
frappe.db.commit()
|
||||
|
||||
def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs):
|
||||
# """Ensure the Bearer token is valid and authorized access to scopes.
|
||||
"""Ensure the Bearer token is valid and authorized access to scopes.
|
||||
|
||||
# OBS! The request.user attribute should be set to the resource owner
|
||||
# associated with this refresh token.
|
||||
OBS! The request.user attribute should be set to the resource owner
|
||||
associated with this refresh token.
|
||||
|
||||
# :param refresh_token: Unicode refresh token
|
||||
# :param client: Client object set by you, see authenticate_client.
|
||||
# :param request: The HTTP Request (oauthlib.common.Request)
|
||||
# :rtype: True or False
|
||||
:param refresh_token: Unicode refresh token
|
||||
:param client: Client object set by you, see authenticate_client.
|
||||
:param request: The HTTP Request (oauthlib.common.Request)
|
||||
:rtype: True or False
|
||||
|
||||
# Method is used by:
|
||||
# - Authorization Code Grant (indirectly by issuing refresh tokens)
|
||||
# - Resource Owner Password Credentials Grant (also indirectly)
|
||||
# - Refresh Token Grant
|
||||
# """
|
||||
Method is used by:
|
||||
- Authorization Code Grant (indirectly by issuing refresh tokens)
|
||||
- Resource Owner Password Credentials Grant (also indirectly)
|
||||
- Refresh Token Grant
|
||||
"""
|
||||
|
||||
otoken = frappe.get_doc("OAuth Bearer Token", {"refresh_token": refresh_token, "status": "Active"})
|
||||
otoken = frappe.get_doc(
|
||||
"OAuth Bearer Token", {"refresh_token": refresh_token, "status": "Active"}
|
||||
)
|
||||
|
||||
if not otoken:
|
||||
return False
|
||||
|
|
@ -287,36 +345,84 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
return True
|
||||
|
||||
# OpenID Connect
|
||||
def get_id_token(self, token, token_handler, request):
|
||||
"""
|
||||
In the OpenID Connect workflows when an ID Token is requested this method is called.
|
||||
Subclasses should implement the construction, signing and optional encryption of the
|
||||
ID Token as described in the OpenID Connect spec.
|
||||
|
||||
In addition to the standard OAuth2 request properties, the request may also contain
|
||||
these OIDC specific properties which are useful to this method:
|
||||
def finalize_id_token(self, id_token, token, token_handler, request):
|
||||
# Check whether frappe server URL is set
|
||||
id_token_header = {"typ": "jwt", "alg": "HS256"}
|
||||
|
||||
- nonce, if workflow is implicit or hybrid and it was provided
|
||||
- claims, if provided to the original Authorization Code request
|
||||
user = frappe.get_doc(
|
||||
"User",
|
||||
frappe.session.user,
|
||||
)
|
||||
|
||||
The token parameter is a dict which may contain an ``access_token`` entry, in which
|
||||
case the resulting ID Token *should* include a calculated ``at_hash`` claim.
|
||||
if request.nonce:
|
||||
id_token["nonce"] = request.nonce
|
||||
|
||||
Similarly, when the request parameter has a ``code`` property defined, the ID Token
|
||||
*should* include a calculated ``c_hash`` claim.
|
||||
userinfo = get_userinfo(user)
|
||||
|
||||
http://openid.net/specs/openid-connect-core-1_0.html (sections `3.1.3.6`_, `3.2.2.10`_, `3.3.2.11`_)
|
||||
if userinfo.get("iss"):
|
||||
id_token["iss"] = userinfo.get("iss")
|
||||
|
||||
.. _`3.1.3.6`: http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
|
||||
.. _`3.2.2.10`: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken
|
||||
.. _`3.3.2.11`: http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken
|
||||
if "openid" in request.scopes:
|
||||
id_token.update(userinfo)
|
||||
|
||||
:param token: A Bearer token dict
|
||||
:param token_handler: the token handler (BearerToken class)
|
||||
:param request: the HTTP Request (oauthlib.common.Request)
|
||||
:return: The ID Token (a JWS signed JWT)
|
||||
"""
|
||||
# the request.scope should be used by the get_id_token() method to determine which claims to include in the resulting id_token
|
||||
id_token_encoded = jwt.encode(
|
||||
payload=id_token,
|
||||
key=request.client.client_secret,
|
||||
algorithm="HS256",
|
||||
headers=id_token_header,
|
||||
)
|
||||
|
||||
return frappe.safe_decode(id_token_encoded)
|
||||
|
||||
def get_authorization_code_nonce(self, client_id, code, redirect_uri, request):
|
||||
if frappe.get_value("OAuth Authorization Code", code, "validity") == "Valid":
|
||||
return frappe.get_value("OAuth Authorization Code", code, "nonce")
|
||||
|
||||
return None
|
||||
|
||||
def get_authorization_code_scopes(self, client_id, code, redirect_uri, request):
|
||||
scope = frappe.get_value("OAuth Client", client_id, "scopes")
|
||||
if not scope:
|
||||
scope = []
|
||||
else:
|
||||
scope = scope.split(get_url_delimiter())
|
||||
|
||||
return scope
|
||||
|
||||
def get_jwt_bearer_token(self, token, token_handler, request):
|
||||
now = datetime.datetime.now()
|
||||
id_token = dict(
|
||||
aud=token.client_id,
|
||||
iat=round(now.timestamp()),
|
||||
at_hash=calculate_at_hash(token.access_token, hashlib.sha256),
|
||||
)
|
||||
return self.finalize_id_token(id_token, token, token_handler, request)
|
||||
|
||||
def get_userinfo_claims(self, request):
|
||||
user = frappe.get_doc("User", frappe.session.user)
|
||||
userinfo = get_userinfo(user)
|
||||
return userinfo
|
||||
|
||||
def validate_id_token(self, token, scopes, request):
|
||||
try:
|
||||
id_token = frappe.get_doc("OAuth Bearer Token", token)
|
||||
if id_token.status == "Active":
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
def validate_jwt_bearer_token(self, token, scopes, request):
|
||||
try:
|
||||
jwt = frappe.get_doc("OAuth Bearer Token", token)
|
||||
if jwt.status == "Active":
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
def validate_silent_authorization(self, request):
|
||||
"""Ensure the logged in user has authorized silent OpenID authorization.
|
||||
|
|
@ -328,9 +434,9 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- OpenIDConnectAuthCode
|
||||
- OpenIDConnectImplicit
|
||||
- OpenIDConnectHybrid
|
||||
- OpenIDConnectAuthCode
|
||||
- OpenIDConnectImplicit
|
||||
- OpenIDConnectHybrid
|
||||
"""
|
||||
if request.prompt == "login":
|
||||
False
|
||||
|
|
@ -351,9 +457,9 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- OpenIDConnectAuthCode
|
||||
- OpenIDConnectImplicit
|
||||
- OpenIDConnectHybrid
|
||||
- OpenIDConnectAuthCode
|
||||
- OpenIDConnectImplicit
|
||||
- OpenIDConnectHybrid
|
||||
"""
|
||||
if frappe.session.user == "Guest" or request.prompt.lower() == "login":
|
||||
return False
|
||||
|
|
@ -373,32 +479,77 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
:rtype: True or False
|
||||
|
||||
Method is used by:
|
||||
- OpenIDConnectAuthCode
|
||||
- OpenIDConnectImplicit
|
||||
- OpenIDConnectHybrid
|
||||
- OpenIDConnectAuthCode
|
||||
- OpenIDConnectImplicit
|
||||
- OpenIDConnectHybrid
|
||||
"""
|
||||
if id_token_hint and id_token_hint == frappe.db.get_value("User Social Login", {"parent":frappe.session.user, "provider": "frappe"}, "userid"):
|
||||
if id_token_hint:
|
||||
try:
|
||||
user = None
|
||||
payload = jwt.decode(
|
||||
id_token_hint,
|
||||
options={
|
||||
"verify_signature": False,
|
||||
"verify_aud": False,
|
||||
},
|
||||
)
|
||||
client_id, client_secret = frappe.get_value(
|
||||
"OAuth Client",
|
||||
payload.get("aud"),
|
||||
["client_id", "client_secret"],
|
||||
)
|
||||
|
||||
if payload.get("sub") and client_id and client_secret:
|
||||
user = frappe.db.get_value(
|
||||
"User Social Login",
|
||||
{"userid": payload.get("sub"), "provider": "frappe"},
|
||||
"parent",
|
||||
)
|
||||
user = frappe.get_doc("User", user)
|
||||
verified_payload = jwt.decode(
|
||||
id_token_hint,
|
||||
key=client_secret,
|
||||
audience=client_id,
|
||||
algorithm="HS256",
|
||||
options={
|
||||
"verify_exp": False,
|
||||
},
|
||||
)
|
||||
|
||||
if verified_payload:
|
||||
return user.name == frappe.session.user
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
elif frappe.session.user != "Guest":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
def validate_user(self, username, password, client, request, *args, **kwargs):
|
||||
"""Ensure the username and password is valid.
|
||||
|
||||
Method is used by:
|
||||
- Resource Owner Password Credentials Grant
|
||||
"""
|
||||
Method is used by:
|
||||
- Resource Owner Password Credentials Grant
|
||||
"""
|
||||
login_manager = LoginManager()
|
||||
login_manager.authenticate(username, password)
|
||||
|
||||
if login_manager.user == "Guest":
|
||||
return False
|
||||
|
||||
request.user = login_manager.user
|
||||
return True
|
||||
|
||||
|
||||
def get_cookie_dict_from_headers(r):
|
||||
cookie = cookies.BaseCookie()
|
||||
if r.headers.get('Cookie'):
|
||||
cookie.load(r.headers.get('Cookie'))
|
||||
if r.headers.get("Cookie"):
|
||||
cookie.load(r.headers.get("Cookie"))
|
||||
return cookie
|
||||
|
||||
|
||||
def calculate_at_hash(access_token, hash_alg):
|
||||
"""Helper method for calculating an access token
|
||||
hash, as described in http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
|
||||
|
|
@ -409,21 +560,25 @@ def calculate_at_hash(access_token, hash_alg):
|
|||
then take the left-most 128 bits and base64url encode them. The at_hash value is a
|
||||
case sensitive string.
|
||||
Args:
|
||||
access_token (str): An access token string.
|
||||
hash_alg (callable): A callable returning a hash object, e.g. hashlib.sha256
|
||||
access_token (str): An access token string.
|
||||
hash_alg (callable): A callable returning a hash object, e.g. hashlib.sha256
|
||||
"""
|
||||
hash_digest = hash_alg(access_token.encode('utf-8')).digest()
|
||||
hash_digest = hash_alg(access_token.encode("utf-8")).digest()
|
||||
cut_at = int(len(hash_digest) / 2)
|
||||
truncated = hash_digest[:cut_at]
|
||||
from jwt.utils import base64url_encode
|
||||
|
||||
at_hash = base64url_encode(truncated)
|
||||
return at_hash.decode('utf-8')
|
||||
return at_hash.decode("utf-8")
|
||||
|
||||
|
||||
def delete_oauth2_data():
|
||||
# Delete Invalid Authorization Code and Revoked Token
|
||||
commit_code, commit_token = False, False
|
||||
code_list = frappe.get_all("OAuth Authorization Code", filters={"validity":"Invalid"})
|
||||
token_list = frappe.get_all("OAuth Bearer Token", filters={"status":"Revoked"})
|
||||
code_list = frappe.get_all(
|
||||
"OAuth Authorization Code", filters={"validity": "Invalid"}
|
||||
)
|
||||
token_list = frappe.get_all("OAuth Bearer Token", filters={"status": "Revoked"})
|
||||
if len(code_list) > 0:
|
||||
commit_code = True
|
||||
if len(token_list) > 0:
|
||||
|
|
@ -439,3 +594,58 @@ def delete_oauth2_data():
|
|||
def get_client_scopes(client_id):
|
||||
scopes_string = frappe.db.get_value("OAuth Client", client_id, "scopes")
|
||||
return scopes_string.split()
|
||||
|
||||
|
||||
def get_userinfo(user):
|
||||
picture = None
|
||||
frappe_server_url = get_server_url()
|
||||
|
||||
if user.user_image:
|
||||
if frappe.utils.validate_url(user.user_image):
|
||||
picture = user.user_image
|
||||
else:
|
||||
picture = frappe_server_url + "/" + user.user_image
|
||||
|
||||
userinfo = frappe._dict(
|
||||
{
|
||||
"sub": frappe.db.get_value(
|
||||
"User Social Login",
|
||||
{"parent": user.name, "provider": "frappe"},
|
||||
"userid",
|
||||
),
|
||||
"name": " ".join(filter(None, [user.first_name, user.last_name])),
|
||||
"given_name": user.first_name,
|
||||
"family_name": user.last_name,
|
||||
"email": user.email,
|
||||
"picture": picture,
|
||||
"roles": frappe.get_roles(user.name),
|
||||
"iss": frappe_server_url,
|
||||
}
|
||||
)
|
||||
|
||||
return userinfo
|
||||
|
||||
|
||||
def get_url_delimiter(separator_character=" "):
|
||||
return separator_character
|
||||
|
||||
|
||||
def generate_json_error_response(e):
|
||||
if not e:
|
||||
e = frappe._dict({})
|
||||
|
||||
frappe.local.response = frappe._dict(
|
||||
{
|
||||
"description": getattr(e, "description", "Internal Server Error"),
|
||||
"status_code": getattr(e, "status_code", 500),
|
||||
"error": getattr(e, "error", "internal_server_error"),
|
||||
}
|
||||
)
|
||||
frappe.local.response["http_status_code"] = getattr(e, "status_code", 500)
|
||||
return
|
||||
|
||||
|
||||
def get_server_url():
|
||||
request_url = urlparse(frappe.request.url)
|
||||
request_url = f"{request_url.scheme}://{request_url.netloc}"
|
||||
return frappe.get_value("Social Login Key", "frappe", "base_url") or request_url
|
||||
|
|
|
|||
|
|
@ -114,8 +114,6 @@ frappe.Application = Class.extend({
|
|||
dialog.get_close_btn().toggle(false);
|
||||
});
|
||||
|
||||
this.setup_user_group_listeners();
|
||||
|
||||
// listen to build errors
|
||||
this.setup_build_error_listener();
|
||||
|
||||
|
|
@ -476,14 +474,19 @@ frappe.Application = Class.extend({
|
|||
$('<link rel="icon" href="' + link + '" type="image/x-icon">').appendTo("head");
|
||||
},
|
||||
trigger_primary_action: function() {
|
||||
if(window.cur_dialog && cur_dialog.display) {
|
||||
// trigger primary
|
||||
cur_dialog.get_primary_btn().trigger("click");
|
||||
} else if(cur_frm && cur_frm.page.btn_primary.is(':visible')) {
|
||||
cur_frm.page.btn_primary.trigger('click');
|
||||
} else if(frappe.container.page.save_action) {
|
||||
frappe.container.page.save_action();
|
||||
}
|
||||
// to trigger change event on active input before triggering primary action
|
||||
$(document.activeElement).blur();
|
||||
// wait for possible JS validations triggered after blur (it might change primary button)
|
||||
setTimeout(() => {
|
||||
if (window.cur_dialog && cur_dialog.display) {
|
||||
// trigger primary
|
||||
cur_dialog.get_primary_btn().trigger("click");
|
||||
} else if (cur_frm && cur_frm.page.btn_primary.is(':visible')) {
|
||||
cur_frm.page.btn_primary.trigger('click');
|
||||
} else if (frappe.container.page.save_action) {
|
||||
frappe.container.page.save_action();
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
|
||||
set_rtl: function() {
|
||||
|
|
@ -593,15 +596,6 @@ frappe.Application = Class.extend({
|
|||
}
|
||||
},
|
||||
|
||||
setup_user_group_listeners() {
|
||||
frappe.realtime.on('user_group_added', (user_group) => {
|
||||
frappe.boot.user_groups && frappe.boot.user_groups.push(user_group);
|
||||
});
|
||||
frappe.realtime.on('user_group_deleted', (user_group) => {
|
||||
frappe.boot.user_groups = (frappe.boot.user_groups || []).filter(el => el !== user_group);
|
||||
});
|
||||
},
|
||||
|
||||
setup_energy_point_listeners() {
|
||||
frappe.realtime.on('energy_point_alert', (message) => {
|
||||
frappe.show_alert(message);
|
||||
|
|
|
|||
|
|
@ -319,7 +319,7 @@ frappe.get_data_pill = (label, target_id=null, remove_action=null, image=null) =
|
|||
|
||||
frappe.get_modal = function(title, content) {
|
||||
return $(`<div class="modal fade" style="overflow: auto;" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-scrollable">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="fill-width flex title-section">
|
||||
|
|
|
|||
|
|
@ -90,16 +90,10 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({
|
|||
});
|
||||
|
||||
this.$input.on("awesomplete-open", () => {
|
||||
this.toggle_container_scroll('.modal-dialog', 'modal-dialog-scrollable');
|
||||
this.toggle_container_scroll('.grid-form-body .form-area', 'scrollable');
|
||||
|
||||
this.autocomplete_open = true;
|
||||
});
|
||||
|
||||
this.$input.on("awesomplete-close", () => {
|
||||
this.toggle_container_scroll('.modal-dialog', 'modal-dialog-scrollable', true);
|
||||
this.toggle_container_scroll('.grid-form-body .form-area', 'scrollable', true);
|
||||
|
||||
this.autocomplete_open = false;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -78,35 +78,25 @@ frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({
|
|||
},
|
||||
|
||||
get_mention_options() {
|
||||
if (!(this.mentions && this.mentions.length)) {
|
||||
if (!this.enable_mentions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const at_values = this.mentions.slice();
|
||||
|
||||
let me = this;
|
||||
return {
|
||||
allowedChars: /^[A-Za-z0-9_]*$/,
|
||||
mentionDenotationChars: ["@"],
|
||||
isolateCharacter: true,
|
||||
source: function (searchTerm, renderList, mentionChar) {
|
||||
let values;
|
||||
|
||||
if (mentionChar === "@") {
|
||||
values = at_values;
|
||||
}
|
||||
|
||||
if (searchTerm.length === 0) {
|
||||
renderList(values, searchTerm);
|
||||
} else {
|
||||
const matches = [];
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (~values[i].value.toLowerCase().indexOf(searchTerm.toLowerCase())) {
|
||||
matches.push(values[i]);
|
||||
}
|
||||
}
|
||||
renderList(matches, searchTerm);
|
||||
}
|
||||
},
|
||||
source: frappe.utils.debounce(async function(search_term, renderList) {
|
||||
let method = me.mention_search_method || 'frappe.desk.search.get_names_for_mentions';
|
||||
let values = await frappe.xcall(method, {
|
||||
search_term
|
||||
});
|
||||
renderList(values, search_term);
|
||||
}, 300),
|
||||
renderItem(item) {
|
||||
let value = item.value;
|
||||
return `${value} ${item.is_group ? frappe.utils.icon('users') : ''}`;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
frappe.ui.form.ControlCurrency = frappe.ui.form.ControlFloat.extend({
|
||||
format_for_input: function(value) {
|
||||
var formatted_value = format_number(value, this.get_number_format(), this.get_precision());
|
||||
return isNaN(parseFloat(value)) ? "" : formatted_value;
|
||||
return isNaN(Number(value)) ? "" : formatted_value;
|
||||
},
|
||||
|
||||
get_precision: function() {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({
|
|||
number_format = this.get_number_format();
|
||||
}
|
||||
var formatted_value = format_number(value, number_format, this.get_precision());
|
||||
return isNaN(parseFloat(value)) ? "" : formatted_value;
|
||||
return isNaN(Number(value)) ? "" : formatted_value;
|
||||
},
|
||||
|
||||
get_number_format: function() {
|
||||
|
|
|
|||
|
|
@ -241,16 +241,10 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
|
|||
});
|
||||
|
||||
this.$input.on("awesomplete-open", () => {
|
||||
this.toggle_container_scroll('.modal-dialog', 'modal-dialog-scrollable');
|
||||
this.toggle_container_scroll('.grid-form-body .form-area', 'scrollable');
|
||||
|
||||
this.autocomplete_open = true;
|
||||
});
|
||||
|
||||
this.$input.on("awesomplete-close", () => {
|
||||
this.toggle_container_scroll('.modal-dialog', 'modal-dialog-scrollable', true);
|
||||
this.toggle_container_scroll('.grid-form-body .form-area', 'scrollable', true);
|
||||
|
||||
this.autocomplete_open = false;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class MentionBlot extends Embed {
|
|||
denotationChar.innerHTML = data.denotationChar;
|
||||
node.appendChild(denotationChar);
|
||||
node.innerHTML += data.value;
|
||||
node.innerHTML += `${data.isGroup === 'true' ? frappe.utils.icon('users') : ''}`;
|
||||
node.dataset.id = data.id;
|
||||
node.dataset.value = data.value;
|
||||
node.dataset.denotationChar = data.denotationChar;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ frappe.ui.form.Footer = Class.extend({
|
|||
parent: this.wrapper.find(".comment-box"),
|
||||
render_input: true,
|
||||
only_input: true,
|
||||
mentions: frappe.utils.get_names_for_mentions(),
|
||||
enable_mentions: true,
|
||||
df: {
|
||||
fieldtype: 'Comment',
|
||||
fieldname: 'comment'
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ class FormTimeline extends BaseTimeline {
|
|||
fieldname: 'comment',
|
||||
label: 'Comment'
|
||||
},
|
||||
mentions: frappe.utils.get_names_for_mentions(),
|
||||
enable_mentions: true,
|
||||
render_input: true,
|
||||
only_input: true,
|
||||
no_wrapper: true
|
||||
|
|
|
|||
|
|
@ -451,7 +451,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
return this.script_manager.trigger("onload_post_render");
|
||||
}
|
||||
},
|
||||
() => this.is_new() && this.focus_on_first_input(),
|
||||
() => this.cscript.is_onload && this.is_new() && this.focus_on_first_input(),
|
||||
() => this.run_after_load_hook(),
|
||||
() => this.dashboard.after_refresh()
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ export default class GridRow {
|
|||
$.extend(this, opts);
|
||||
if (this.doc && this.parent_df.options) {
|
||||
frappe.meta.make_docfield_copy_for(this.parent_df.options, this.doc.name, this.docfields);
|
||||
this.docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name);
|
||||
const docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name);
|
||||
this.docfields = docfields.length ? docfields : opts.docfields;
|
||||
}
|
||||
this.columns = {};
|
||||
this.columns_list = [];
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export default class GridRowForm {
|
|||
</div>
|
||||
</div>
|
||||
<div class="grid-form-body">
|
||||
<div class="form-area scrollable"></div>
|
||||
<div class="form-area"></div>
|
||||
<div class="grid-footer-toolbar hidden-xs flex justify-between">
|
||||
<div class="grid-shortcuts">
|
||||
<span> ${frappe.utils.icon("keyboard", "md")} </span>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
}
|
||||
set_title() {
|
||||
if (this.frm.is_new()) {
|
||||
var title = __('New {0}', [this.frm.meta.name]);
|
||||
var title = __('New {0}', [__(this.frm.meta.name)]);
|
||||
} else if (this.frm.meta.title_field) {
|
||||
let title_field = (this.frm.doc[this.frm.meta.title_field] || "").toString().trim();
|
||||
var title = strip_html(title_field || this.frm.docname);
|
||||
|
|
@ -551,7 +551,7 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
|
||||
let fields = this.frm.fields
|
||||
.filter(visible_fields_filter)
|
||||
.map(f => ({ label: f.df.label, value: f.df.fieldname }));
|
||||
.map(f => ({ label: __(f.df.label), value: f.df.fieldname }));
|
||||
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __('Jump to field'),
|
||||
|
|
|
|||
|
|
@ -150,13 +150,13 @@ frappe.views.ListViewSelect = class ListViewSelect {
|
|||
const views_wrapper = this.sidebar.sidebar.find(".views-section");
|
||||
views_wrapper.find(".sidebar-label").html(`${__(view)}`);
|
||||
const $dropdown = views_wrapper.find(".views-dropdown");
|
||||
|
||||
let placeholder = `Select ${view}`;
|
||||
|
||||
let placeholder = `${__("Select {0}", [__(view)])}`;
|
||||
let html = ``;
|
||||
|
||||
if (!items || !items.length) {
|
||||
html = `<div class="empty-state">
|
||||
${__("No {} Found", [view])}
|
||||
${__("No {0} Found", [__(view)])}
|
||||
</div>`;
|
||||
} else {
|
||||
const page_name = this.get_page_name();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<div class="tag-filters-area">
|
||||
<div class="active-tag-filters">
|
||||
<button class="btn btn-default btn-xs add-filter text-muted">
|
||||
Add Filter
|
||||
{{ __("Add Filter") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -71,12 +71,12 @@
|
|||
</div>
|
||||
<div v-if="requests.length == 0" class="no-result text-muted flex justify-center align-center" style="">
|
||||
<div class="msg-box no-border" v-if="status.status == 'Inactive'" >
|
||||
<p>Recorder is Inactive</p>
|
||||
<p><button class="btn btn-primary btn-sm btn-new-doc" @click="start()">Start Recording</button></p>
|
||||
<p>{{ __("Recorder is Inactive") }}</p>
|
||||
<p><button class="btn btn-primary btn-sm btn-new-doc" @click="start()">{{ __("Start Recording") }}</button></p>
|
||||
</div>
|
||||
<div class="msg-box no-border" v-if="status.status == 'Active'" >
|
||||
<p>No Requests found</p>
|
||||
<p>Go make some noise</p>
|
||||
<p>{{ __("No Requests found") }}</p>
|
||||
<p>{{ __("Go make some noise") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="requests.length != 0" class="list-paging-area">
|
||||
|
|
@ -108,12 +108,12 @@ export default {
|
|||
return {
|
||||
requests: [],
|
||||
columns: [
|
||||
{label: "Path", slug: "path"},
|
||||
{label: "Duration (ms)", slug: "duration", sortable: true, number: true},
|
||||
{label: "Time in Queries (ms)", slug: "time_queries", sortable: true, number: true},
|
||||
{label: "Queries", slug: "queries", sortable: true, number: true},
|
||||
{label: "Method", slug: "method"},
|
||||
{label: "Time", slug: "time", sortable: true},
|
||||
{label: __("Path"), slug: "path"},
|
||||
{label: __("Duration (ms)"), slug: "duration", sortable: true, number: true},
|
||||
{label: __("Time in Queries (ms)"), slug: "time_queries", sortable: true, number: true},
|
||||
{label: __("Queries"), slug: "queries", sortable: true, number: true},
|
||||
{label: __("Method"), slug: "method"},
|
||||
{label: __("Time"), slug: "time", sortable: true},
|
||||
],
|
||||
query: {
|
||||
sort: "duration",
|
||||
|
|
@ -140,7 +140,7 @@ export default {
|
|||
mounted() {
|
||||
this.fetch_status();
|
||||
this.refresh();
|
||||
this.$root.page.set_secondary_action("Clear", () => {
|
||||
this.$root.page.set_secondary_action(__("Clear"), () => {
|
||||
frappe.set_route("recorder");
|
||||
this.clear();
|
||||
});
|
||||
|
|
@ -151,11 +151,11 @@ export default {
|
|||
const current_page = this.query.pagination.page;
|
||||
const total_pages = this.query.pagination.total;
|
||||
return [{
|
||||
label: "First",
|
||||
label: __("First"),
|
||||
number: 1,
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
},{
|
||||
label: "Previous",
|
||||
label: __("Previous"),
|
||||
number: Math.max(current_page - 1, 1),
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
}, {
|
||||
|
|
@ -163,11 +163,11 @@ export default {
|
|||
number: current_page,
|
||||
status: "btn-info",
|
||||
}, {
|
||||
label: "Next",
|
||||
label: __("Next"),
|
||||
number: Math.min(current_page + 1, total_pages),
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}, {
|
||||
label: "Last",
|
||||
label: __("Last"),
|
||||
number: total_pages,
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}];
|
||||
|
|
@ -230,11 +230,11 @@ export default {
|
|||
},
|
||||
update_buttons: function() {
|
||||
if(this.status.status == "Active") {
|
||||
this.$root.page.set_primary_action("Stop", () => {
|
||||
this.$root.page.set_primary_action(__("Stop"), () => {
|
||||
this.stop();
|
||||
});
|
||||
} else {
|
||||
this.$root.page.set_primary_action("Start", () => {
|
||||
this.$root.page.set_primary_action(__("Start"), () => {
|
||||
this.start();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
</div>
|
||||
<div class="row form-section visible-section">
|
||||
<div class="col-sm-10">
|
||||
<h6 class="form-section-heading uppercase">SQL Queries</h6>
|
||||
<h6 class="form-section-heading uppercase">{{ __("SQL Queries") }}</h6>
|
||||
</div>
|
||||
<div class="col-sm-2 filter-list">
|
||||
<div class="sort-selector">
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
<div class="checkbox">
|
||||
<label>
|
||||
<span class="input-area"><input type="checkbox" class="input-with-feedback bold" data-fieldtype="Check" v-model="group_duplicates"></span>
|
||||
<span class="label-area small">Group Duplicate Queries</span>
|
||||
<span class="label-area small">{{ __("Group Duplicate Queries") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -48,15 +48,15 @@
|
|||
<div class="grid-row">
|
||||
<div class="data-row row">
|
||||
<div class="row-index col col-xs-1">
|
||||
<span>Index</span></div>
|
||||
<span>{{ __("Index") }}</span></div>
|
||||
<div class="col grid-static-col col-xs-6">
|
||||
<div class="static-area ellipsis">Query</div>
|
||||
<div class="static-area ellipsis">{{ __("Query") }}</div>
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2">
|
||||
<div class="static-area ellipsis text-right">Duration (ms)</div>
|
||||
<div class="static-area ellipsis text-right">{{ __("Duration (ms)") }}</div>
|
||||
</div>
|
||||
<div class="col grid-static-col col-xs-2">
|
||||
<div class="static-area ellipsis text-right">Exact Copies</div>
|
||||
<div class="static-area ellipsis text-right">{{ __("Exact Copies") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -82,7 +82,7 @@
|
|||
<div class="recorder-form-in-grid" v-if="showing == call.index">
|
||||
<div class="grid-form-heading" @click="showing = null">
|
||||
<div class="toolbar grid-header-toolbar">
|
||||
<span class="panel-title">SQL Query #<span class="grid-form-row-index">{{ call.index }}</span></span>
|
||||
<span class="panel-title">{{ __("SQL Query") }} #<span class="grid-form-row-index">{{ call.index }}</span></span>
|
||||
<div class="btn btn-default btn-xs pull-right" style="margin-left: 7px;">
|
||||
<span class="hidden-xs octicon octicon-triangle-up"></span>
|
||||
</div>
|
||||
|
|
@ -98,25 +98,25 @@
|
|||
<form>
|
||||
<div class="frappe-control">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">Query</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ __("Query") }}</label></div>
|
||||
<div class="control-value like-disabled-input for-description"><pre>{{ call.query }}</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="frappe-control input-max-width">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">Duration (ms)</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ __("Duration (ms)") }}</label></div>
|
||||
<div class="control-value like-disabled-input">{{ call.duration }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="frappe-control input-max-width">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">Exact Copies</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ __("Exact Copies") }}</label></div>
|
||||
<div class="control-value like-disabled-input">{{ call.exact_copies }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="frappe-control">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">Stack Trace</label></div>
|
||||
<div class="clearfix"><label class="control-label"{{ __("Stack Trace") }}</label></div>
|
||||
<div class="control-value like-disabled-input for-description" style="overflow:auto">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
|
|
@ -137,7 +137,7 @@
|
|||
</div>
|
||||
<div class="frappe-control" v-if="call.explain_result[0]">
|
||||
<div class="form-group">
|
||||
<div class="clearfix"><label class="control-label">SQL Explain</label></div>
|
||||
<div class="clearfix"><label class="control-label">{{ __("SQL Explain") }}</label></div>
|
||||
<div class="control-value like-disabled-input for-description" style="overflow:auto">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="request.calls.length == 0" class="grid-empty text-center">No Data</div>
|
||||
<div v-if="request.calls.length == 0" class="grid-empty text-center">{{ __("No Data") }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -201,19 +201,19 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
columns: [
|
||||
{label: "Path", slug: "path", type: "Data", class: "col-sm-6"},
|
||||
{label: "CMD", slug: "cmd", type: "Data", class: "col-sm-6"},
|
||||
{label: "Time", slug: "time", type: "Time", class: "col-sm-6"},
|
||||
{label: "Duration (ms)", slug: "duration", type: "Float", class: "col-sm-6"},
|
||||
{label: "Number of Queries", slug: "queries", type: "Int", class: "col-sm-6"},
|
||||
{label: "Time in Queries (ms)", slug: "time_queries", type: "Float", class: "col-sm-6"},
|
||||
{label: "Request Headers", slug: "headers", type: "Small Text", formatter: value => `<pre class="for-description like-disabled-input">${JSON.stringify(value, null, 4)}</pre>`, class: "col-sm-12"},
|
||||
{label: "Form Dict", slug: "form_dict", type: "Small Text", formatter: value => `<pre class="for-description like-disabled-input">${JSON.stringify(value, null, 4)}</pre>`, class: "col-sm-12"},
|
||||
{label: __("Path"), slug: "path", type: "Data", class: "col-sm-6"},
|
||||
{label: __("CMD"), slug: "cmd", type: "Data", class: "col-sm-6"},
|
||||
{label: __("Time"), slug: "time", type: "Time", class: "col-sm-6"},
|
||||
{label: __("Duration (ms)"), slug: "duration", type: "Float", class: "col-sm-6"},
|
||||
{label: __("Number of Queries"), slug: "queries", type: "Int", class: "col-sm-6"},
|
||||
{label: __("Time in Queries (ms)"), slug: "time_queries", type: "Float", class: "col-sm-6"},
|
||||
{label: __("Request Headers"), slug: "headers", type: "Small Text", formatter: value => `<pre class="for-description like-disabled-input">${JSON.stringify(value, null, 4)}</pre>`, class: "col-sm-12"},
|
||||
{label: __("Form Dict"), slug: "form_dict", type: "Small Text", formatter: value => `<pre class="for-description like-disabled-input">${JSON.stringify(value, null, 4)}</pre>`, class: "col-sm-12"},
|
||||
],
|
||||
table_columns: [
|
||||
{label: "Execution Order", slug: "index", sortable: true},
|
||||
{label: "Duration (ms)", slug: "duration", sortable: true},
|
||||
{label: "Exact Copies", slug: "exact_copies", sortable: true},
|
||||
{label: __("Execution Order"), slug: "index", sortable: true},
|
||||
{label: __("Duration (ms)"), slug: "duration", sortable: true},
|
||||
{label: __("Exact Copies"), slug: "exact_copies", sortable: true},
|
||||
],
|
||||
query: {
|
||||
sort: "duration",
|
||||
|
|
@ -236,11 +236,11 @@ export default {
|
|||
const current_page = this.query.pagination.page;
|
||||
const total_pages = this.query.pagination.total;
|
||||
return [{
|
||||
label: "First",
|
||||
label: __("First"),
|
||||
number: 1,
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
},{
|
||||
label: "Previous",
|
||||
label: __("Previous"),
|
||||
number: Math.max(current_page - 1, 1),
|
||||
status: (current_page == 1) ? "disabled" : "",
|
||||
}, {
|
||||
|
|
@ -248,11 +248,11 @@ export default {
|
|||
number: current_page,
|
||||
status: "btn-info",
|
||||
}, {
|
||||
label: "Next",
|
||||
label: __("Next"),
|
||||
number: Math.min(current_page + 1, total_pages),
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}, {
|
||||
label: "Last",
|
||||
label: __("Last"),
|
||||
number: total_pages,
|
||||
status: (current_page == total_pages) ? "disabled" : "",
|
||||
}];
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import RecorderRoot from "./RecorderRoot.vue";
|
|||
import RecorderDetail from "./RecorderDetail.vue";
|
||||
import RequestDetail from "./RequestDetail.vue";
|
||||
|
||||
Vue.prototype.__ = window.__;
|
||||
Vue.prototype.frappe = window.frappe;
|
||||
|
||||
Vue.use(VueRouter);
|
||||
const routes = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -36,18 +36,6 @@ frappe.ui.FieldSelect = Class.extend({
|
|||
var item = me.awesomplete.get_item(value);
|
||||
me.$input.val(item.label);
|
||||
});
|
||||
this.$input.on("awesomplete-open", () => {
|
||||
let modal = this.$input.parents('.modal-dialog')[0];
|
||||
if (modal) {
|
||||
$(modal).removeClass("modal-dialog-scrollable");
|
||||
}
|
||||
});
|
||||
this.$input.on("awesomplete-close", () => {
|
||||
let modal = this.$input.parents('.modal-dialog')[0];
|
||||
if (modal) {
|
||||
$(modal).addClass("modal-dialog-scrollable");
|
||||
}
|
||||
});
|
||||
|
||||
if(this.filter_fields) {
|
||||
for(var i in this.filter_fields)
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ class NotificationsView extends BaseNotificationsView {
|
|||
this.container.append($(`<div class="notification-null-state">
|
||||
<div class="text-center">
|
||||
<img src="/assets/frappe/images/ui-states/notification-empty-state.svg" alt="Generic Empty State" class="null-state">
|
||||
<div class="title">No New notifications</div>
|
||||
<div class="title">${__('No New notifications')}</div>
|
||||
<div class="subtitle">
|
||||
${__('Looks like you haven’t received any notifications.')}
|
||||
</div></div></div>`));
|
||||
|
|
@ -430,7 +430,7 @@ class EventsView extends BaseNotificationsView {
|
|||
<div class="notification-null-state">
|
||||
<div class="text-center">
|
||||
<img src="/assets/frappe/images/ui-states/event-empty-state.svg" alt="Generic Empty State" class="null-state">
|
||||
<div class="title">No Upcoming Events</div>
|
||||
<div class="title">${__('No Upcoming Events')}</div>
|
||||
<div class="subtitle">
|
||||
${__('There are no upcoming events for you.')}
|
||||
</div></div></div>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,34 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
|
|||
title: __("Switch Theme")
|
||||
});
|
||||
this.body = $(`<div class="theme-grid"></div>`).appendTo(this.dialog.$body);
|
||||
this.bind_events();
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
this.dialog.$wrapper.on('keydown', (e) => {
|
||||
if (!this.themes) return;
|
||||
|
||||
const key = frappe.ui.keys.get_key(e);
|
||||
let increment_by;
|
||||
|
||||
if (key === "right") {
|
||||
increment_by = 1;
|
||||
} else if (key === "left") {
|
||||
increment_by = -1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const current_index = this.themes.findIndex(theme => {
|
||||
return theme.name === this.current_theme;
|
||||
});
|
||||
|
||||
const new_theme = this.themes[current_index + increment_by];
|
||||
if (!new_theme) return;
|
||||
|
||||
new_theme.$html.click();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
|
|
|
|||
|
|
@ -1272,31 +1272,6 @@ Object.assign(frappe.utils, {
|
|||
</div>`);
|
||||
},
|
||||
|
||||
get_names_for_mentions() {
|
||||
let names_for_mentions = Object.keys(frappe.boot.user_info || [])
|
||||
.filter(user => {
|
||||
return !["Administrator", "Guest"].includes(user)
|
||||
&& frappe.boot.user_info[user].allowed_in_mentions
|
||||
&& frappe.boot.user_info[user].user_type === 'System User';
|
||||
})
|
||||
.map(user => {
|
||||
return {
|
||||
id: frappe.boot.user_info[user].name,
|
||||
value: frappe.boot.user_info[user].fullname,
|
||||
};
|
||||
});
|
||||
|
||||
frappe.boot.user_groups && frappe.boot.user_groups.map(group => {
|
||||
names_for_mentions.push({
|
||||
id: group,
|
||||
value: group,
|
||||
is_group: true,
|
||||
link: frappe.utils.get_form_link('User Group', group)
|
||||
});
|
||||
});
|
||||
|
||||
return names_for_mentions;
|
||||
},
|
||||
print(doctype, docname, print_format, letterhead, lang_code) {
|
||||
let w = window.open(
|
||||
frappe.urllib.get_full_url(
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ frappe.breadcrumbs = {
|
|||
if (breadcrumbs.doctype && ["print", "form"].includes(view)) {
|
||||
this.set_list_breadcrumb(breadcrumbs);
|
||||
this.set_form_breadcrumb(breadcrumbs, view);
|
||||
} else if (breadcrumbs.doctype && view === 'list') {
|
||||
this.set_list_breadcrumb(breadcrumbs);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
|
|||
setup_defaults() {
|
||||
return super.setup_defaults()
|
||||
.then(() => {
|
||||
this.page_title = __('{0} Dashboard', [this.doctype]);
|
||||
this.page_title = __('{0} Dashboard', [__(this.doctype)]);
|
||||
this.dashboard_settings = frappe.get_user_settings(this.doctype)['dashboard_settings'] || null;
|
||||
});
|
||||
}
|
||||
|
|
@ -271,7 +271,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
|
|||
show_add_chart_dialog() {
|
||||
let fields = this.get_field_options();
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Add a {0} Chart", [this.doctype]),
|
||||
title: __("Add a {0} Chart", [__(this.doctype)]),
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'new_or_existing',
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// TODO: Refactor for better UX
|
||||
|
||||
frappe.provide("frappe.views");
|
||||
|
||||
(function() {
|
||||
|
|
@ -185,7 +187,7 @@ frappe.provide("frappe.views");
|
|||
new_index: card.new_index,
|
||||
};
|
||||
}
|
||||
|
||||
frappe.dom.freeze();
|
||||
frappe.call({
|
||||
method: method_prefix + method_name,
|
||||
args: args,
|
||||
|
|
@ -198,6 +200,7 @@ frappe.provide("frappe.views");
|
|||
cards: cards,
|
||||
columns: columns
|
||||
});
|
||||
frappe.dom.unfreeze();
|
||||
}
|
||||
}).fail(function() {
|
||||
// revert original order
|
||||
|
|
@ -205,6 +208,7 @@ frappe.provide("frappe.views");
|
|||
cards: _cards,
|
||||
columns: _columns
|
||||
});
|
||||
frappe.dom.unfreeze();
|
||||
});
|
||||
},
|
||||
update_order: function(updater) {
|
||||
|
|
|
|||
|
|
@ -335,12 +335,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
let message;
|
||||
if (dashboard_name) {
|
||||
let dashboard_route_html = `<a href="#dashboard-view/${dashboard_name}">${dashboard_name}</a>`;
|
||||
message = __("New {0} {1} added to Dashboard {2}", [doctype, name, dashboard_route_html]);
|
||||
message = __("New {0} {1} added to Dashboard {2}", [__(doctype), name, dashboard_route_html]);
|
||||
} else {
|
||||
message = __("New {0} {1} created", [doctype, name]);
|
||||
message = __("New {0} {1} created", [__(doctype), name]);
|
||||
}
|
||||
|
||||
frappe.msgprint(message, __("New {0} Created", [doctype]));
|
||||
frappe.msgprint(message, __("New {0} Created", [__(doctype)]));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -937,7 +937,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
else {
|
||||
wrapper[0].innerHTML =
|
||||
`<div class="flex justify-center align-center text-muted" style="height: 120px; display: flex;">
|
||||
<div>Please select X and Y fields</div>
|
||||
<div>${__("Please select X and Y fields")}</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1094,7 +1094,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
|
||||
return Object.assign(column, {
|
||||
id: column.fieldname,
|
||||
name: __(column.label),
|
||||
name: __(column.label, null, `Column of report '${this.report_name}'`), // context has to match context in get_messages_from_report in translate.py
|
||||
width: parseInt(column.width) || null,
|
||||
editable: false,
|
||||
compareValue: compareFn,
|
||||
|
|
@ -1343,7 +1343,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
|
||||
open_url_post(frappe.request.url, args);
|
||||
}
|
||||
}, __('Export Report: '+ this.report_name), __('Download'));
|
||||
}, __('Export Report: {0}', [this.report_name]), __('Download'));
|
||||
}
|
||||
|
||||
get_data_for_csv(include_indentation) {
|
||||
|
|
|
|||
|
|
@ -87,11 +87,13 @@ export default class WebForm extends frappe.ui.FieldGroup {
|
|||
}
|
||||
|
||||
setup_delete_button() {
|
||||
this.add_button_to_header(
|
||||
frappe.utils.icon('delete'),
|
||||
"danger",
|
||||
() => this.delete()
|
||||
);
|
||||
frappe.has_permission(this.doc_type, "", "delete", () => {
|
||||
this.add_button_to_header(
|
||||
frappe.utils.icon('delete'),
|
||||
"danger",
|
||||
() => this.delete()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
setup_print_button() {
|
||||
|
|
|
|||
|
|
@ -190,9 +190,11 @@ export default class WebFormList {
|
|||
make_actions() {
|
||||
const actions = document.querySelector(".list-view-actions");
|
||||
|
||||
this.addButton(actions, "delete-rows", "danger", true, "Delete", () =>
|
||||
this.delete_rows()
|
||||
);
|
||||
frappe.has_permission(this.doctype, "", "delete", () => {
|
||||
this.addButton(actions, "delete-rows", "danger", true, "Delete", () =>
|
||||
this.delete_rows()
|
||||
);
|
||||
});
|
||||
|
||||
this.addButton(
|
||||
actions,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import Widget from "./base_widget.js";
|
|||
|
||||
frappe.provide("frappe.utils");
|
||||
|
||||
const INDICATOR_COLORS = ["Grey", "Green", "Red", "Orange", "Pink", "Yellow", "Blue", "Cyan", "Teal"];
|
||||
export default class ShortcutWidget extends Widget {
|
||||
constructor(opts) {
|
||||
opts.shadow = true;
|
||||
|
|
@ -79,7 +78,7 @@ export default class ShortcutWidget extends Widget {
|
|||
|
||||
this.action_area.empty();
|
||||
const label = get_label();
|
||||
let color = INDICATOR_COLORS.includes(this.color) && count ? this.color.toLowerCase() : 'gray';
|
||||
let color = this.color && count ? this.color.toLowerCase() : 'gray';
|
||||
$(`<div class="indicator-pill ellipsis ${color}">${label}</div>`).appendTo(this.action_area);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,9 +237,19 @@ class ShortcutDialog extends WidgetDialog {
|
|||
hidden: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Color",
|
||||
fieldtype: "Select",
|
||||
fieldname: "color",
|
||||
label: __("Color"),
|
||||
options: ["Grey", "Green", "Red", "Orange", "Pink", "Yellow", "Blue", "Cyan"],
|
||||
default: "Grey",
|
||||
onchange: () => {
|
||||
let color = this.dialog.fields_dict.color.value.toLowerCase();
|
||||
let $select = this.dialog.fields_dict.color.$input;
|
||||
if (!$select.parent().find('.color-box').get(0)) {
|
||||
$(`<div class="color-box"></div>`).insertBefore($select.get(0));
|
||||
}
|
||||
$select.parent().find('.color-box').get(0).style.backgroundColor = `var(--text-on-${color})`;
|
||||
}
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,17 @@
|
|||
--blue-100: #D3E9FC;
|
||||
--blue-50 : #F0F8FE;
|
||||
|
||||
--cyan-900: #006464;
|
||||
--cyan-800: #007272;
|
||||
--cyan-700: #008b8b;
|
||||
--cyan-600: #02c5c5;
|
||||
--cyan-500: #00ffff;
|
||||
--cyan-400: #2ef8f8;
|
||||
--cyan-300: #6efcfc;
|
||||
--cyan-200: #a0f8f8;
|
||||
--cyan-100: #c7fcfc;
|
||||
--cyan-50 : #dafafa;
|
||||
|
||||
--green-900: #2D401D;
|
||||
--green-800: #44622A;
|
||||
--green-700: #518B21;
|
||||
|
|
@ -151,6 +162,8 @@
|
|||
--bg-gray: var(--gray-200);
|
||||
--bg-light-gray: var(--gray-100);
|
||||
--bg-purple: var(--purple-100);
|
||||
--bg-pink: var(--pink-50);
|
||||
--bg-cyan: var(--cyan-50);
|
||||
|
||||
--text-on-blue: var(--blue-600);
|
||||
--text-on-light-blue: var(--blue-500);
|
||||
|
|
@ -163,6 +176,8 @@
|
|||
--text-on-gray: var(--gray-600);
|
||||
--text-on-light-gray: var(--gray-800);
|
||||
--text-on-purple: var(--purple-500);
|
||||
--text-on-pink: var(--pink-500);
|
||||
--text-on-cyan: var(--cyan-600);
|
||||
|
||||
--awesomplete-hover-bg: var(--control-bg);
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,22 @@ input[type="checkbox"] {
|
|||
@include card();
|
||||
}
|
||||
|
||||
.frappe-control[data-fieldtype="Select"].frappe-control[data-fieldname="color"] {
|
||||
select {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.color-box {
|
||||
position: absolute;
|
||||
top: calc(50% - 11px);
|
||||
left: 8px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 5px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.frappe-control[data-fieldtype="Select"] .control-input,
|
||||
.frappe-control[data-fieldtype="Select"].form-group {
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -77,6 +77,16 @@
|
|||
@include indicator-pill-color('green');
|
||||
}
|
||||
|
||||
.indicator.cyan {
|
||||
@include indicator-color('cyan');
|
||||
}
|
||||
|
||||
.indicator-pill.cyan,
|
||||
.indicator-pill-right.cyan,
|
||||
.indicator-pill-round.cyan {
|
||||
@include indicator-pill-color('cyan');
|
||||
}
|
||||
|
||||
.indicator.blue {
|
||||
@include indicator-color('blue');
|
||||
}
|
||||
|
|
@ -131,6 +141,16 @@
|
|||
@include indicator-pill-color('red');
|
||||
}
|
||||
|
||||
.indicator.pink {
|
||||
@include indicator-color('pink');
|
||||
}
|
||||
|
||||
.indicator-pill.pink,
|
||||
.indicator-pill-right.pink,
|
||||
.indicator-pill-round.pink {
|
||||
@include indicator-pill-color('pink');
|
||||
}
|
||||
|
||||
.indicator-pill.darkgrey,
|
||||
.indicator-pill-right.darkgrey,
|
||||
.indicator-pill-round.darkgrey {
|
||||
|
|
|
|||
|
|
@ -2,25 +2,50 @@ h5.modal-title {
|
|||
margin: 0px !important;
|
||||
}
|
||||
|
||||
body.modal-open {
|
||||
overflow: auto;
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
// Hack to fix incorrect padding applied by Bootstrap
|
||||
body.modal-open[style^="padding-right"] {
|
||||
padding-right: 12px !important;
|
||||
|
||||
header.navbar {
|
||||
padding-right: 12px !important;
|
||||
margin-right: -12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
// Same scrollbar as body
|
||||
scrollbar-width: auto;
|
||||
&::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
// Hide scrollbar on touch devices
|
||||
@media(max-width: 991px) {
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
.modal-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
background: inherit;
|
||||
padding: var(--padding-md) var(--padding-lg);
|
||||
padding-bottom: 0;
|
||||
border-bottom: 0;
|
||||
// padding-bottom: 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
.modal-title {
|
||||
font-weight: 500;
|
||||
line-height: 2em;
|
||||
font-size: $font-size-lg;
|
||||
max-width: calc(100% - 80px);
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
|
|
@ -60,9 +85,17 @@ body.modal-open {
|
|||
}
|
||||
}
|
||||
|
||||
.awesomplete ul {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
background: inherit;
|
||||
padding: var(--padding-md) var(--padding-lg);
|
||||
border-top: 0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
justify-content: space-between;
|
||||
|
||||
button {
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@
|
|||
padding: 10px 12px;
|
||||
height: initial;
|
||||
line-height: initial;
|
||||
cursor: pointer;
|
||||
|
||||
&.selected {
|
||||
background-color: var(--control-bg);
|
||||
|
|
@ -163,7 +164,7 @@
|
|||
}
|
||||
|
||||
.ql-editor td {
|
||||
border: 1px solid var(--border-color);
|
||||
border: 1px solid var(--dark-border-color);
|
||||
}
|
||||
|
||||
.ql-editor blockquote {
|
||||
|
|
@ -196,5 +197,8 @@
|
|||
}
|
||||
|
||||
.mention[data-is-group="true"] {
|
||||
background-color: var(--group-mention-bg-color);
|
||||
.icon {
|
||||
margin-top: -2px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,8 @@
|
|||
.summary-item {
|
||||
// SIZE & SPACING
|
||||
margin: 0px 30px;
|
||||
width: 180px;
|
||||
min-width: 180px;
|
||||
max-width: 300px;
|
||||
height: 62px;
|
||||
|
||||
// LAYOUT
|
||||
|
|
|
|||
|
|
@ -9,11 +9,6 @@ html {
|
|||
}
|
||||
|
||||
/* Works on Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb-color);
|
||||
}
|
||||
|
|
@ -23,7 +18,12 @@ html {
|
|||
background: var(--scrollbar-track-color);
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar {
|
||||
width: unset;
|
||||
height: unset;
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ class TestCommands(BaseTestCommands):
|
|||
|
||||
# test 7: take a backup with frappe.conf.backup.includes
|
||||
self.execute(
|
||||
"bench --site {site} set-config backup '{includes}' --as-dict",
|
||||
"bench --site {site} set-config backup '{includes}' --parse",
|
||||
{"includes": json.dumps(backup["includes"])},
|
||||
)
|
||||
self.execute("bench --site {site} backup --verbose")
|
||||
|
|
@ -226,7 +226,7 @@ class TestCommands(BaseTestCommands):
|
|||
|
||||
# test 8: take a backup with frappe.conf.backup.excludes
|
||||
self.execute(
|
||||
"bench --site {site} set-config backup '{excludes}' --as-dict",
|
||||
"bench --site {site} set-config backup '{excludes}' --parse",
|
||||
{"excludes": json.dumps(backup["excludes"])},
|
||||
)
|
||||
self.execute("bench --site {site} backup --verbose")
|
||||
|
|
@ -365,6 +365,43 @@ class TestCommands(BaseTestCommands):
|
|||
installed_apps = set(frappe.get_installed_apps())
|
||||
self.assertSetEqual(list_apps, installed_apps)
|
||||
|
||||
# test 3: parse json format
|
||||
self.execute("bench --site all list-apps --format json")
|
||||
self.assertEquals(self.returncode, 0)
|
||||
self.assertIsInstance(json.loads(self.stdout), dict)
|
||||
|
||||
self.execute("bench --site {site} list-apps --format json")
|
||||
self.assertIsInstance(json.loads(self.stdout), dict)
|
||||
|
||||
self.execute("bench --site {site} list-apps -f json")
|
||||
self.assertIsInstance(json.loads(self.stdout), dict)
|
||||
|
||||
def test_show_config(self):
|
||||
# test 1: sanity check for command
|
||||
self.execute("bench --site all show-config")
|
||||
self.assertEquals(self.returncode, 0)
|
||||
|
||||
# test 2: test keys in table text
|
||||
self.execute(
|
||||
"bench --site {site} set-config test_key '{second_order}' --parse",
|
||||
{"second_order": json.dumps({"test_key": "test_value"})},
|
||||
)
|
||||
self.execute("bench --site {site} show-config")
|
||||
self.assertEquals(self.returncode, 0)
|
||||
self.assertIn("test_key.test_key", self.stdout.split())
|
||||
self.assertIn("test_value", self.stdout.split())
|
||||
|
||||
# test 3: parse json format
|
||||
self.execute("bench --site all show-config --format json")
|
||||
self.assertEquals(self.returncode, 0)
|
||||
self.assertIsInstance(json.loads(self.stdout), dict)
|
||||
|
||||
self.execute("bench --site {site} show-config --format json")
|
||||
self.assertIsInstance(json.loads(self.stdout), dict)
|
||||
|
||||
self.execute("bench --site {site} show-config -f json")
|
||||
self.assertIsInstance(json.loads(self.stdout), dict)
|
||||
|
||||
def test_get_bench_relative_path(self):
|
||||
bench_path = frappe.utils.get_bench_path()
|
||||
test1_path = os.path.join(bench_path, "test1.txt")
|
||||
|
|
|
|||
|
|
@ -2,10 +2,14 @@
|
|||
# MIT License. See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest, frappe, requests, time
|
||||
from frappe.test_runner import make_test_records
|
||||
import unittest
|
||||
import requests
|
||||
import jwt
|
||||
from six.moves.urllib.parse import urlparse, parse_qs, urljoin
|
||||
from urllib.parse import urlencode, quote
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.integrations.oauth2 import encode_params
|
||||
|
||||
class TestOAuth20(unittest.TestCase):
|
||||
|
|
@ -34,11 +38,7 @@ class TestOAuth20(unittest.TestCase):
|
|||
self.assertFalse(check_valid_openid_response())
|
||||
|
||||
def test_login_using_authorization_code(self):
|
||||
client = frappe.get_doc("OAuth Client", self.client_id)
|
||||
client.grant_type = "Authorization Code"
|
||||
client.response_type = "Code"
|
||||
client.save()
|
||||
frappe.db.commit()
|
||||
update_client_for_auth_code_grant(self.client_id)
|
||||
|
||||
session = requests.Session()
|
||||
login(session)
|
||||
|
|
@ -71,7 +71,8 @@ class TestOAuth20(unittest.TestCase):
|
|||
"grant_type": "authorization_code",
|
||||
"code": auth_code,
|
||||
"redirect_uri": self.redirect_uri,
|
||||
"client_id": self.client_id
|
||||
"client_id": self.client_id,
|
||||
"scope": self.scope,
|
||||
})
|
||||
)
|
||||
|
||||
|
|
@ -86,6 +87,54 @@ class TestOAuth20(unittest.TestCase):
|
|||
self.assertTrue(bearer_token.get("token_type") == "Bearer")
|
||||
self.assertTrue(check_valid_openid_response(bearer_token.get("access_token")))
|
||||
|
||||
def test_login_using_authorization_code_with_pkce(self):
|
||||
update_client_for_auth_code_grant(self.client_id)
|
||||
|
||||
session = requests.Session()
|
||||
login(session)
|
||||
|
||||
redirect_destination = None
|
||||
|
||||
# Go to Authorize url
|
||||
try:
|
||||
session.get(
|
||||
get_full_url("/api/method/frappe.integrations.oauth2.authorize"),
|
||||
params=encode_params({
|
||||
"client_id": self.client_id,
|
||||
"scope": self.scope,
|
||||
"response_type": "code",
|
||||
"redirect_uri": self.redirect_uri,
|
||||
"code_challenge_method": 'S256',
|
||||
"code_challenge": '21XaP8MJjpxCMRxgEzBP82sZ73PRLqkyBUta1R309J0' ,
|
||||
})
|
||||
)
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
redirect_destination = ex.request.url
|
||||
|
||||
# Get authorization code from redirected URL
|
||||
query = parse_qs(urlparse(redirect_destination).query)
|
||||
auth_code = query.get("code")[0]
|
||||
|
||||
# Request for bearer token
|
||||
token_response = requests.post(
|
||||
get_full_url("/api/method/frappe.integrations.oauth2.get_token"),
|
||||
headers=self.form_header,
|
||||
data=encode_params({
|
||||
"grant_type": "authorization_code",
|
||||
"code": auth_code,
|
||||
"redirect_uri": self.redirect_uri,
|
||||
"client_id": self.client_id,
|
||||
"scope": self.scope,
|
||||
"code_verifier": "420",
|
||||
})
|
||||
)
|
||||
|
||||
# Parse bearer token json
|
||||
bearer_token = token_response.json()
|
||||
|
||||
self.assertTrue(bearer_token.get("access_token"))
|
||||
self.assertTrue(bearer_token.get("id_token"))
|
||||
|
||||
def test_revoke_token(self):
|
||||
client = frappe.get_doc("OAuth Client", self.client_id)
|
||||
client.grant_type = "Authorization Code"
|
||||
|
|
@ -203,6 +252,61 @@ class TestOAuth20(unittest.TestCase):
|
|||
self.assertTrue(response_dict.get("token_type"))
|
||||
self.assertTrue(check_valid_openid_response(response_dict.get("access_token")[0]))
|
||||
|
||||
def test_openid_code_id_token(self):
|
||||
client = update_client_for_auth_code_grant(self.client_id)
|
||||
|
||||
session = requests.Session()
|
||||
login(session)
|
||||
|
||||
redirect_destination = None
|
||||
|
||||
nonce = frappe.generate_hash()
|
||||
|
||||
# Go to Authorize url
|
||||
try:
|
||||
session.get(
|
||||
get_full_url("/api/method/frappe.integrations.oauth2.authorize"),
|
||||
params=encode_params({
|
||||
"client_id": self.client_id,
|
||||
"scope": self.scope,
|
||||
"response_type": "code",
|
||||
"redirect_uri": self.redirect_uri,
|
||||
"nonce": nonce,
|
||||
})
|
||||
)
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
redirect_destination = ex.request.url
|
||||
|
||||
# Get authorization code from redirected URL
|
||||
query = parse_qs(urlparse(redirect_destination).query)
|
||||
auth_code = query.get("code")[0]
|
||||
|
||||
# Request for bearer token
|
||||
token_response = requests.post(
|
||||
get_full_url("/api/method/frappe.integrations.oauth2.get_token"),
|
||||
headers=self.form_header,
|
||||
data=encode_params({
|
||||
"grant_type": "authorization_code",
|
||||
"code": auth_code,
|
||||
"redirect_uri": self.redirect_uri,
|
||||
"client_id": self.client_id,
|
||||
"scope": self.scope,
|
||||
})
|
||||
)
|
||||
|
||||
# Parse bearer token json
|
||||
bearer_token = token_response.json()
|
||||
|
||||
id_token = bearer_token.get("id_token")
|
||||
payload = jwt.decode(
|
||||
id_token,
|
||||
audience=client.client_id,
|
||||
key=client.client_secret,
|
||||
algorithm="HS256",
|
||||
)
|
||||
|
||||
self.assertTrue(payload.get("nonce") == nonce)
|
||||
|
||||
|
||||
def check_valid_openid_response(access_token=None):
|
||||
"""Return True for valid response."""
|
||||
|
|
@ -233,3 +337,12 @@ def login(session):
|
|||
def get_full_url(endpoint):
|
||||
"""Turn '/endpoint' into 'http://127.0.0.1:8000/endpoint'."""
|
||||
return urljoin(frappe.utils.get_url(), endpoint)
|
||||
|
||||
|
||||
def update_client_for_auth_code_grant(client_id):
|
||||
client = frappe.get_doc("OAuth Client", client_id)
|
||||
client.grant_type = "Authorization Code"
|
||||
client.response_type = "Code"
|
||||
client.save()
|
||||
frappe.db.commit()
|
||||
return client
|
||||
|
|
|
|||
|
|
@ -443,8 +443,16 @@ def get_messages_from_report(name):
|
|||
messages = _get_messages_from_page_or_report("Report", name,
|
||||
frappe.db.get_value("DocType", report.ref_doctype, "module"))
|
||||
|
||||
if report.columns:
|
||||
context = "Column of report '%s'" % report.name # context has to match context in `prepare_columns` in query_report.js
|
||||
messages.extend([(None, report_column.label, context) for report_column in report.columns])
|
||||
|
||||
if report.filters:
|
||||
messages.extend([(None, report_filter.label) for report_filter in report.filters])
|
||||
|
||||
if report.query:
|
||||
messages.extend([(None, message) for message in re.findall('"([^:,^"]*):', report.query) if is_translatable(message)])
|
||||
|
||||
messages.append((None,report.report_name))
|
||||
return messages
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ from email.utils import formataddr, parseaddr
|
|||
from gzip import GzipFile
|
||||
from typing import Generator, Iterable
|
||||
|
||||
from six import string_types, text_type
|
||||
from six.moves.urllib.parse import quote
|
||||
from urllib.parse import quote, urlparse
|
||||
from werkzeug.test import Client
|
||||
|
||||
import frappe
|
||||
|
|
@ -72,7 +71,7 @@ def get_formatted_email(user, mail=None):
|
|||
def extract_email_id(email):
|
||||
"""fetch only the email part of the Email Address"""
|
||||
email_id = parse_addr(email)[1]
|
||||
if email_id and isinstance(email_id, string_types) and not isinstance(email_id, text_type):
|
||||
if email_id and isinstance(email_id, str) and not isinstance(email_id, str):
|
||||
email_id = email_id.decode("utf-8", "ignore")
|
||||
return email_id
|
||||
|
||||
|
|
@ -370,14 +369,14 @@ def get_site_url(site):
|
|||
|
||||
def encode_dict(d, encoding="utf-8"):
|
||||
for key in d:
|
||||
if isinstance(d[key], string_types) and isinstance(d[key], text_type):
|
||||
if isinstance(d[key], str) and isinstance(d[key], str):
|
||||
d[key] = d[key].encode(encoding)
|
||||
|
||||
return d
|
||||
|
||||
def decode_dict(d, encoding="utf-8"):
|
||||
for key in d:
|
||||
if isinstance(d[key], string_types) and not isinstance(d[key], text_type):
|
||||
if isinstance(d[key], str) and not isinstance(d[key], str):
|
||||
d[key] = d[key].decode(encoding, "ignore")
|
||||
|
||||
return d
|
||||
|
|
@ -644,7 +643,7 @@ def parse_json(val):
|
|||
"""
|
||||
Parses json if string else return
|
||||
"""
|
||||
if isinstance(val, string_types):
|
||||
if isinstance(val, str):
|
||||
val = json.loads(val)
|
||||
if isinstance(val, dict):
|
||||
val = frappe._dict(val)
|
||||
|
|
@ -813,3 +812,11 @@ def groupby_metric(iterable: typing.Dict[str, list], key: str):
|
|||
for item in items:
|
||||
records.setdefault(item[key], {}).setdefault(category, []).append(item)
|
||||
return records
|
||||
|
||||
def validate_url(url_string):
|
||||
try:
|
||||
result = urlparse(url_string)
|
||||
return result.scheme and result.scheme in ["http", "https", "ftp", "ftps"]
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals, print_function
|
||||
|
|
@ -126,16 +126,12 @@ recursive-include {app_name} *.svg
|
|||
recursive-include {app_name} *.txt
|
||||
recursive-exclude {app_name} *.pyc"""
|
||||
|
||||
init_template = """# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
init_template = """
|
||||
__version__ = '0.0.1'
|
||||
|
||||
"""
|
||||
|
||||
hooks_template = """# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
from . import __version__ as app_version
|
||||
hooks_template = """from . import __version__ as app_version
|
||||
|
||||
app_name = "{app_name}"
|
||||
app_title = "{app_title}"
|
||||
|
|
@ -312,9 +308,7 @@ user_data_fields = [
|
|||
|
||||
"""
|
||||
|
||||
desktop_template = """# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
desktop_template = """from frappe import _
|
||||
|
||||
def get_data():
|
||||
return [
|
||||
|
|
@ -328,8 +322,7 @@ def get_data():
|
|||
]
|
||||
"""
|
||||
|
||||
setup_template = """# -*- coding: utf-8 -*-
|
||||
from setuptools import setup, find_packages
|
||||
setup_template = """from setuptools import setup, find_packages
|
||||
|
||||
with open('requirements.txt') as f:
|
||||
install_requires = f.read().strip().split('\\n')
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import functools
|
||||
import requests
|
||||
from terminaltables import AsciiTable
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1024)
|
||||
def get_first_party_apps():
|
||||
"""Get list of all apps under orgs: frappe. erpnext from GitHub"""
|
||||
import requests
|
||||
|
||||
apps = []
|
||||
for org in ["frappe", "erpnext"]:
|
||||
req = requests.get(f"https://api.github.com/users/{org}/repos", {"type": "sources", "per_page": 200})
|
||||
|
|
@ -15,6 +14,8 @@ def get_first_party_apps():
|
|||
|
||||
|
||||
def render_table(data):
|
||||
from terminaltables import AsciiTable
|
||||
|
||||
print(AsciiTable(data).table)
|
||||
|
||||
|
||||
|
|
@ -49,3 +50,9 @@ def log(message, colour=''):
|
|||
colour = colours.get(colour, "")
|
||||
end_line = '\033[0m'
|
||||
print(colour + message + end_line)
|
||||
|
||||
|
||||
def warn(message, category=None):
|
||||
from warnings import warn
|
||||
|
||||
warn(message=message, category=category, stacklevel=2)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@
|
|||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"email",
|
||||
"status"
|
||||
"status",
|
||||
"anonymization_matrix",
|
||||
"deletion_steps"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -27,10 +29,23 @@
|
|||
"label": "Status",
|
||||
"options": "Pending Verification\nPending Approval\nDeleted",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "anonymization_matrix",
|
||||
"fieldtype": "Code",
|
||||
"label": "Anonymization Matrix",
|
||||
"options": "JSON",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "deletion_steps",
|
||||
"fieldtype": "Table",
|
||||
"label": "Deletion Steps ",
|
||||
"options": "Personal Data Deletion Step"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-02-28 12:36:08.219719",
|
||||
"modified": "2021-04-23 13:25:53.629308",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Personal Data Deletion Request",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ from frappe.model.document import Document
|
|||
from frappe.utils import get_fullname
|
||||
from frappe.utils.user import get_system_managers
|
||||
from frappe.utils.verified_command import get_signed_params, verify_request
|
||||
import json
|
||||
from frappe.core.utils import find
|
||||
|
||||
|
||||
class PersonalDataDeletionRequest(Document):
|
||||
|
|
@ -118,6 +120,24 @@ class PersonalDataDeletionRequest(Document):
|
|||
now=frappe.flags.in_test,
|
||||
)
|
||||
|
||||
def add_deletion_steps(self):
|
||||
if self.deletion_steps:
|
||||
return
|
||||
|
||||
for step in self.full_match_privacy_docs + self.partial_privacy_docs:
|
||||
row_data = {
|
||||
"status": "Pending",
|
||||
"document_type": step.get("doctype"),
|
||||
"partial": step.get("partial") or False,
|
||||
"fields": json.dumps(step.get("redact_fields", [])),
|
||||
"filtered_by": step.get("filtered_by") or "",
|
||||
}
|
||||
self.append("deletion_steps", row_data)
|
||||
|
||||
self.anonymization_matrix = json.dumps(self.anonymization_value_map, indent=4)
|
||||
self.save()
|
||||
self.reload()
|
||||
|
||||
def redact_partial_match_data(self, doctype):
|
||||
self.__redact_partial_match_data(doctype)
|
||||
self.rename_documents(doctype)
|
||||
|
|
@ -143,11 +163,11 @@ class PersonalDataDeletionRequest(Document):
|
|||
|
||||
def redact_full_match_data(self, ref, email):
|
||||
"""Replaces the entire field value by the values set in the anonymization_value_map"""
|
||||
filter_by = ref["filter_by"]
|
||||
filter_by = ref.get("filter_by", "owner")
|
||||
|
||||
docs = frappe.get_all(
|
||||
ref["doctype"],
|
||||
filters={filter_by: ("like", "%" + email + "%")},
|
||||
filters={filter_by: email},
|
||||
fields=["name", filter_by],
|
||||
)
|
||||
|
||||
|
|
@ -185,7 +205,7 @@ class PersonalDataDeletionRequest(Document):
|
|||
return anonymize_fields_dict
|
||||
|
||||
def redact_doc(self, doc, ref):
|
||||
filter_by = ref["filter_by"]
|
||||
filter_by = ref.get("filter_by", "owner")
|
||||
meta = frappe.get_meta(ref["doctype"])
|
||||
filter_by_meta = meta.get_field(filter_by)
|
||||
|
||||
|
|
@ -207,21 +227,57 @@ class PersonalDataDeletionRequest(Document):
|
|||
ref["doctype"], doc["name"], self.anon, force=True, show_alert=False
|
||||
)
|
||||
|
||||
def _anonymize_data(self, email=None, anon=None, set_data=True):
|
||||
def _anonymize_data(self, email=None, anon=None, set_data=True, commit=False):
|
||||
email = email or self.email
|
||||
anon = anon or self.name
|
||||
|
||||
if set_data:
|
||||
self.__set_anonymization_data(email, anon)
|
||||
|
||||
for doctype in self.full_match_privacy_docs:
|
||||
self.redact_full_match_data(doctype, email)
|
||||
self.add_deletion_steps()
|
||||
|
||||
for doctype in self.partial_privacy_docs:
|
||||
self.full_match_doctypes = (
|
||||
x
|
||||
for x in self.full_match_privacy_docs
|
||||
if filter(
|
||||
lambda x: x.document_type == x and x.status == "Pending", self.deletion_steps
|
||||
)
|
||||
)
|
||||
|
||||
self.partial_match_doctypes = (
|
||||
x
|
||||
for x in self.partial_privacy_docs
|
||||
if filter(
|
||||
lambda x: x.document_type == x and x.status == "Pending", self.deletion_steps
|
||||
)
|
||||
)
|
||||
|
||||
for doctype in self.full_match_doctypes:
|
||||
self.redact_full_match_data(doctype, email)
|
||||
self.set_step_status(doctype["doctype"])
|
||||
if commit:
|
||||
frappe.db.commit()
|
||||
|
||||
for doctype in self.partial_match_doctypes:
|
||||
self.redact_partial_match_data(doctype)
|
||||
self.set_step_status(doctype["doctype"])
|
||||
if commit:
|
||||
frappe.db.commit()
|
||||
|
||||
frappe.rename_doc("User", email, anon, force=True, show_alert=False)
|
||||
self.db_set("status", "Deleted")
|
||||
if commit:
|
||||
frappe.db.commit()
|
||||
|
||||
def set_step_status(self, step, status="Deleted"):
|
||||
del_step = find(self.deletion_steps, lambda x: x.document_type == step and x.status != status)
|
||||
|
||||
if not del_step:
|
||||
del_step = find(self.deletion_steps, lambda x: x.document_type == step)
|
||||
|
||||
del_step.status = status
|
||||
self.save()
|
||||
self.reload()
|
||||
|
||||
def __set_anonymization_data(self, email, anon):
|
||||
self.anon = anon or self.name
|
||||
|
|
@ -290,9 +346,8 @@ def confirm_deletion(email, name, host_name):
|
|||
frappe.db.commit()
|
||||
frappe.respond_as_web_page(
|
||||
_("Confirmed"),
|
||||
_(
|
||||
"The process for deletion of {0} data associated with {1} has been initiated."
|
||||
).format(host_name, email),
|
||||
_("The process for deletion of {0} data associated with {1} has been initiated.")
|
||||
.format(host_name, email),
|
||||
indicator_color="green",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2021-04-23 13:25:26.162797",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"document_type",
|
||||
"status",
|
||||
"partial",
|
||||
"fields",
|
||||
"filtered_by"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"fieldname": "document_type",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Document Type",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_preview": 1,
|
||||
"label": "Status",
|
||||
"options": "Pending\nDeleted",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "partial",
|
||||
"fieldtype": "Check",
|
||||
"in_preview": 1,
|
||||
"label": "Partial",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "fields",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Fields",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "filtered_by",
|
||||
"fieldtype": "Data",
|
||||
"label": "Filtered By",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-23 13:48:59.658681",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Personal Data Deletion Step",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class PersonalDataDeletionStep(Document):
|
||||
pass
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Fieldtype",
|
||||
"options": "Attach\nAttach Image\nCheck\nCurrency\nData\nDate\nDatetime\nDuration\nFloat\nHTML\nInt\nLink\nRating\nSelect\nSmall Text\nText\nText Editor\nTable\nSection Break\nColumn Break"
|
||||
"options": "Attach\nAttach Image\nCheck\nCurrency\nData\nDate\nDatetime\nDuration\nFloat\nHTML\nInt\nLink\nPassword\nRating\nSelect\nSmall Text\nText\nText Editor\nTable\nSection Break\nColumn Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "label",
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-10 23:20:44.354862",
|
||||
"modified": "2021-04-30 12:02:25.422345",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Web Form Field",
|
||||
|
|
|
|||
2
frappe/website/js/bootstrap-4.js
vendored
2
frappe/website/js/bootstrap-4.js
vendored
|
|
@ -21,7 +21,7 @@ $('.dropdown-menu a.dropdown-toggle').on('click', function (e) {
|
|||
frappe.get_modal = function (title, content) {
|
||||
return $(
|
||||
`<div class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${title}</h5>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@
|
|||
{%- endif -%}
|
||||
<div class="split-section-content col-12 {{ left_col if image_on_right else right_col }} {{ align_content }}">
|
||||
<h2>{{ title }}</h2>
|
||||
{%- if content -%}
|
||||
<p>{{ content }}</p>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if link_label and link_url -%}
|
||||
<a href="{{ link_url }}">{{ link_label }}</a>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue