Merge branch 'develop' into default-desk-page
This commit is contained in:
commit
0363d787e1
56 changed files with 1048 additions and 99 deletions
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
|
|
@ -15,7 +15,7 @@ If your issue is not clear or does not meet the guidelines, then it will be clos
|
|||
### General Issue Guidelines
|
||||
|
||||
1. **Search existing Issues:** Before raising a Issue, search if it has been raised before. Maybe add a 👍 or give additional help by creating a mockup if it is not already created.
|
||||
2. **Report each issue separately:** Don't club multiple, unreleated issues in one note.
|
||||
2. **Report each issue separately:** Don't club multiple, unrelated issues in one note.
|
||||
3. **Brief:** Please don't include long explanations. Use screenshots and bullet points instead of descriptive paragraphs.
|
||||
|
||||
### Bug Report Guidelines
|
||||
|
|
|
|||
|
|
@ -104,11 +104,11 @@ install:
|
|||
|
||||
- cd ./frappe-bench
|
||||
|
||||
- sed -i 's/watch:/# watch:/g' Procfile
|
||||
- sed -i 's/schedule:/# schedule:/g' Procfile
|
||||
- sed -i 's/^watch:/# watch:/g' Procfile
|
||||
- sed -i 's/^schedule:/# schedule:/g' Procfile
|
||||
|
||||
- if [ $TYPE == "server" ]; then sed -i 's/socketio:/# socketio:/g' Procfile; fi
|
||||
- if [ $TYPE == "server" ]; then sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile; fi
|
||||
- if [ $TYPE == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; fi
|
||||
- if [ $TYPE == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi
|
||||
|
||||
- if [ $TYPE == "ui" ]; then bench setup requirements --node; fi
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
"repeat_on_last_day",
|
||||
"column_break_12",
|
||||
"next_schedule_date",
|
||||
"section_break_12",
|
||||
"section_break_16",
|
||||
"repeat_on_days",
|
||||
"notification",
|
||||
"notify_by_email",
|
||||
|
|
@ -198,20 +198,20 @@
|
|||
"label": "Repeat on Days",
|
||||
"options": "Auto Repeat Day"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.frequency==='Weekly';",
|
||||
"fieldname": "section_break_12",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "submit_on_creation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Submit on Creation"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.frequency==='Weekly';",
|
||||
"fieldname": "section_break_16",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-12-10 10:43:13.449172",
|
||||
"modified": "2021-01-12 09:24:49.719611",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Auto Repeat",
|
||||
|
|
|
|||
|
|
@ -77,6 +77,11 @@ def get_data():
|
|||
"name": "OAuth Provider Settings",
|
||||
"description": _("Settings for OAuth Provider"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Connected App",
|
||||
"description": _("Connect to any OAuth Provider"),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -751,7 +751,7 @@ class Row:
|
|||
self.warnings.append(
|
||||
{
|
||||
"row": self.row_number,
|
||||
"message": _("{0} is a mandatory field asdadsf").format(id_field.label),
|
||||
"message": _("{0} is a mandatory field").format(id_field.label),
|
||||
}
|
||||
)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -642,10 +642,15 @@
|
|||
"group": "Activity",
|
||||
"link_doctype": "ToDo",
|
||||
"link_fieldname": "owner"
|
||||
},
|
||||
{
|
||||
"group": "Integrations",
|
||||
"link_doctype": "Token Cache",
|
||||
"link_fieldname": "user"
|
||||
}
|
||||
],
|
||||
"max_attachments": 5,
|
||||
"modified": "2020-08-26 19:48:49.677800",
|
||||
"modified": "2020-10-18 15:18:53.126800",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "User",
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ def has_permission(doc, ptype, user):
|
|||
if doc.report_name in allowed_reports:
|
||||
return True
|
||||
else:
|
||||
allowed_doctypes = [frappe.permissions.get_doctypes_with_read()]
|
||||
allowed_doctypes = frappe.permissions.get_doctypes_with_read()
|
||||
if doc.document_type in allowed_doctypes:
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat
|
|||
|
||||
except Exception:
|
||||
frappe.errprint(frappe.utils.get_traceback())
|
||||
frappe.msgprint(frappe._("Did not cancel"))
|
||||
raise
|
||||
|
||||
def send_updated_docs(doc):
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ def install():
|
|||
|
||||
@frappe.whitelist()
|
||||
def update_genders():
|
||||
default_genders = [_("Male"), _("Female"), _("Other"),_("Transgender"), _("Genderqueer"), _("Non-Conforming"),_("Prefer not to say")]
|
||||
default_genders = ["Male", "Female", "Other","Transgender", "Genderqueer", "Non-Conforming","Prefer not to say"]
|
||||
records = [{'doctype': 'Gender', 'gender': d} for d in default_genders]
|
||||
for record in records:
|
||||
frappe.get_doc(record).insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_salutations():
|
||||
default_salutations = [_("Mr"), _("Ms"), _('Mx'), _("Dr"), _("Mrs"), _("Madam"), _("Miss"), _("Master"), _("Prof")]
|
||||
default_salutations = ["Mr", "Ms", 'Mx', "Dr", "Mrs", "Madam", "Miss", "Master", "Prof"]
|
||||
records = [{'doctype': 'Salutation', 'salutation': d} for d in default_salutations]
|
||||
for record in records:
|
||||
doc = frappe.new_doc(record.get("doctype"))
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ class AutoEmailReport(Document):
|
|||
|
||||
if self.format == 'HTML':
|
||||
columns, data = make_links(columns, data)
|
||||
|
||||
columns = update_field_types(columns)
|
||||
return self.get_html_table(columns, data)
|
||||
|
||||
elif self.format == 'XLSX':
|
||||
|
|
@ -236,5 +236,14 @@ def make_links(columns, data):
|
|||
elif col.fieldtype == "Dynamic Link":
|
||||
if col.options and row.get(col.fieldname) and row.get(col.options):
|
||||
row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname])
|
||||
elif col.fieldtype == "Currency":
|
||||
row[col.fieldname] = frappe.format_value(row[col.fieldname], col)
|
||||
|
||||
return columns, data
|
||||
|
||||
def update_field_types(columns):
|
||||
for col in columns:
|
||||
if col.fieldtype in ("Link", "Dynamic Link", "Currency") and col.options != "Currency":
|
||||
col.fieldtype = "Data"
|
||||
col.options = ""
|
||||
return columns
|
||||
|
|
@ -210,7 +210,7 @@ class EmailAccount(Document):
|
|||
elif not in_receive and any(map(lambda t: t in message, auth_error_codes)):
|
||||
self.throw_invalid_credentials_exception()
|
||||
else:
|
||||
frappe.throw(e)
|
||||
frappe.throw(cstr(e))
|
||||
|
||||
except socket.error:
|
||||
if in_receive:
|
||||
|
|
|
|||
|
|
@ -2,58 +2,66 @@
|
|||
# License: GNU General Public License v3. See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe, unittest
|
||||
from frappe.utils import getdate, add_days
|
||||
import unittest
|
||||
from random import choice
|
||||
|
||||
from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe, send_scheduled_email
|
||||
from six.moves.urllib.parse import unquote
|
||||
import frappe
|
||||
from frappe.email.doctype.newsletter.newsletter import (
|
||||
confirmed_unsubscribe,
|
||||
send_scheduled_email,
|
||||
)
|
||||
from frappe.email.doctype.newsletter.newsletter import get_newsletter_list
|
||||
from frappe.email.queue import flush
|
||||
from frappe.utils import add_days, getdate
|
||||
|
||||
test_dependencies = ["Email Group"]
|
||||
emails = [
|
||||
"test_subscriber1@example.com",
|
||||
"test_subscriber2@example.com",
|
||||
"test_subscriber3@example.com",
|
||||
"test1@example.com",
|
||||
]
|
||||
|
||||
emails = ["test_subscriber1@example.com", "test_subscriber2@example.com",
|
||||
"test_subscriber3@example.com", "test1@example.com"]
|
||||
|
||||
class TestNewsletter(unittest.TestCase):
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql('delete from `tabEmail Group Member`')
|
||||
frappe.db.sql("delete from `tabEmail Group Member`")
|
||||
|
||||
if not frappe.db.exists("Email Group", "_Test Email Group"):
|
||||
frappe.get_doc({"doctype": "Email Group", "title": "_Test Email Group"}).insert()
|
||||
|
||||
group_exist=frappe.db.exists("Email Group", "_Test Email Group")
|
||||
if len(group_exist) == 0:
|
||||
frappe.get_doc({
|
||||
"doctype": "Email Group",
|
||||
"title": "_Test Email Group"
|
||||
}).insert()
|
||||
for email in emails:
|
||||
frappe.get_doc({
|
||||
"doctype": "Email Group Member",
|
||||
"email": email,
|
||||
"email_group": "_Test Email Group"
|
||||
}).insert()
|
||||
frappe.get_doc({
|
||||
"doctype": "Email Group Member",
|
||||
"email": email,
|
||||
"email_group": "_Test Email Group"
|
||||
}).insert()
|
||||
|
||||
def test_send(self):
|
||||
name = self.send_newsletter()
|
||||
self.send_newsletter()
|
||||
|
||||
email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")]
|
||||
email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")]
|
||||
self.assertEqual(len(email_queue_list), 4)
|
||||
recipients = [e.recipients[0].recipient for e in email_queue_list]
|
||||
for email in emails:
|
||||
self.assertTrue(email in recipients)
|
||||
|
||||
recipients = set([e.recipients[0].recipient for e in email_queue_list])
|
||||
self.assertTrue(set(emails).issubset(recipients))
|
||||
|
||||
def test_unsubscribe(self):
|
||||
# test unsubscribe
|
||||
name = self.send_newsletter()
|
||||
from frappe.email.queue import flush
|
||||
to_unsubscribe = choice(emails)
|
||||
group = frappe.get_all("Newsletter Email Group", filters={"parent": name}, fields=["email_group"])
|
||||
|
||||
flush(from_test=True)
|
||||
to_unsubscribe = unquote(frappe.local.flags.signed_query_string.split("email=")[1].split("&")[0])
|
||||
group = frappe.get_all("Newsletter Email Group", filters={"parent" : name}, fields=["email_group"])
|
||||
confirmed_unsubscribe(to_unsubscribe, group[0].email_group)
|
||||
|
||||
name = self.send_newsletter()
|
||||
|
||||
email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")]
|
||||
email_queue_list = [
|
||||
frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")
|
||||
]
|
||||
self.assertEqual(len(email_queue_list), 3)
|
||||
recipients = [e.recipients[0].recipient for e in email_queue_list]
|
||||
|
||||
for email in emails:
|
||||
if email != to_unsubscribe:
|
||||
self.assertTrue(email in recipients)
|
||||
|
|
@ -86,7 +94,6 @@ class TestNewsletter(unittest.TestCase):
|
|||
def test_portal(self):
|
||||
self.send_newsletter(1)
|
||||
frappe.set_user("test1@example.com")
|
||||
from frappe.email.doctype.newsletter.newsletter import get_newsletter_list
|
||||
newsletters = get_newsletter_list("Newsletter", None, None, 0)
|
||||
self.assertEqual(len(newsletters), 1)
|
||||
|
||||
|
|
@ -106,4 +113,4 @@ class TestNewsletter(unittest.TestCase):
|
|||
self.assertEqual(len(email_queue_list), 4)
|
||||
recipients = [e.recipients[0].recipient for e in email_queue_list]
|
||||
for email in emails:
|
||||
self.assertTrue(email in recipients)
|
||||
self.assertTrue(email in recipients)
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ def set_update(update, producer_site):
|
|||
if data.changed:
|
||||
local_doc.update(data.changed)
|
||||
if data.removed:
|
||||
update_row_removed(local_doc, data.removed)
|
||||
local_doc = update_row_removed(local_doc, data.removed)
|
||||
if data.row_changed:
|
||||
update_row_changed(local_doc, data.row_changed)
|
||||
if data.added:
|
||||
|
|
@ -318,7 +318,17 @@ def update_row_removed(local_doc, removed):
|
|||
for tablename, rownames in iteritems(removed):
|
||||
table = local_doc.get_table_field_doctype(tablename)
|
||||
for row in rownames:
|
||||
frappe.db.delete(table, row)
|
||||
table_rows = local_doc.get(tablename)
|
||||
child_table_row = get_child_table_row(table_rows, row)
|
||||
table_rows.remove(child_table_row)
|
||||
local_doc.set(tablename, table_rows)
|
||||
return local_doc
|
||||
|
||||
|
||||
def get_child_table_row(table_rows, row):
|
||||
for entry in table_rows:
|
||||
if entry.get('name') == row:
|
||||
return entry
|
||||
|
||||
|
||||
def update_row_changed(local_doc, changed):
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
{
|
||||
"hidden": 0,
|
||||
"label": "Authentication",
|
||||
"links": "[\n {\n \"description\": \"Enter keys to enable login via Facebook, Google, GitHub.\",\n \"label\": \"Social Login Key\",\n \"name\": \"Social Login Key\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Ldap settings\",\n \"label\": \"LDAP Settings\",\n \"name\": \"LDAP Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Register OAuth Client App\",\n \"label\": \"OAuth Client\",\n \"name\": \"OAuth Client\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for OAuth Provider\",\n \"label\": \"OAuth Provider Settings\",\n \"name\": \"OAuth Provider Settings\",\n \"type\": \"doctype\"\n }\n]"
|
||||
"links": "[\n {\n \"description\": \"Enter keys to enable login via Facebook, Google, GitHub.\",\n \"label\": \"Social Login Key\",\n \"name\": \"Social Login Key\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Ldap settings\",\n \"label\": \"LDAP Settings\",\n \"name\": \"LDAP Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Register OAuth Client App\",\n \"label\": \"OAuth Client\",\n \"name\": \"OAuth Client\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for OAuth Provider\",\n \"label\": \"OAuth Provider Settings\",\n \"name\": \"OAuth Provider Settings\",\n \"type\": \"doctype\"\n }\n ,\n {\n \"description\": \"Connect to any OAuth Provider\",\n \"label\": \"Connected App\",\n \"name\": \"Connected App\",\n \"type\": \"doctype\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
|
|
|
|||
0
frappe/integrations/doctype/connected_app/__init__.py
Normal file
0
frappe/integrations/doctype/connected_app/__init__.py
Normal file
38
frappe/integrations/doctype/connected_app/connected_app.js
Normal file
38
frappe/integrations/doctype/connected_app/connected_app.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) 2019, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Connected App', {
|
||||
refresh: frm => {
|
||||
frm.add_custom_button(__('Get OpenID Configuration'), async () => {
|
||||
if (!frm.doc.openid_configuration) {
|
||||
frappe.msgprint(__('Please enter OpenID Configuration URL'));
|
||||
} else {
|
||||
try {
|
||||
const response = await fetch(frm.doc.openid_configuration);
|
||||
const oidc = await response.json();
|
||||
frm.set_value('authorization_uri', oidc.authorization_endpoint);
|
||||
frm.set_value('token_uri', oidc.token_endpoint);
|
||||
frm.set_value('userinfo_uri', oidc.userinfo_endpoint);
|
||||
frm.set_value('introspection_uri', oidc.introspection_endpoint);
|
||||
frm.set_value('revocation_uri', oidc.revocation_endpoint);
|
||||
} catch (error) {
|
||||
frappe.msgprint(__('Please check OpenID Configuration URL'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!frm.is_new()) {
|
||||
frm.add_custom_button(__('Connect to {}', [frm.doc.provider_name]), async () => {
|
||||
frappe.call({
|
||||
method: 'initiate_web_application_flow',
|
||||
doc: frm.doc,
|
||||
callback: function(r) {
|
||||
window.open(r.message, '_blank');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
frm.toggle_display('sb_client_credentials_section', !frm.is_new());
|
||||
}
|
||||
});
|
||||
166
frappe/integrations/doctype/connected_app/connected_app.json
Normal file
166
frappe/integrations/doctype/connected_app/connected_app.json
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
{
|
||||
"actions": [],
|
||||
"beta": 1,
|
||||
"creation": "2019-01-24 15:51:06.362222",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"provider_name",
|
||||
"cb_00",
|
||||
"openid_configuration",
|
||||
"sb_client_credentials_section",
|
||||
"client_id",
|
||||
"redirect_uri",
|
||||
"cb_01",
|
||||
"client_secret",
|
||||
"sb_scope_section",
|
||||
"scopes",
|
||||
"sb_endpoints_section",
|
||||
"authorization_uri",
|
||||
"token_uri",
|
||||
"revocation_uri",
|
||||
"cb_02",
|
||||
"userinfo_uri",
|
||||
"introspection_uri",
|
||||
"section_break_18",
|
||||
"query_parameters"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "provider_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Provider Name",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cb_00",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "openid_configuration",
|
||||
"fieldtype": "Data",
|
||||
"label": "OpenID Configuration"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "sb_client_credentials_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Client Credentials"
|
||||
},
|
||||
{
|
||||
"fieldname": "client_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Client Id"
|
||||
},
|
||||
{
|
||||
"fieldname": "redirect_uri",
|
||||
"fieldtype": "Data",
|
||||
"label": "Redirect URI",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cb_01",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "client_secret",
|
||||
"fieldtype": "Password",
|
||||
"label": "Client Secret"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "sb_scope_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Scopes"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "sb_endpoints_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Endpoints"
|
||||
},
|
||||
{
|
||||
"fieldname": "cb_02",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "scopes",
|
||||
"fieldtype": "Table",
|
||||
"label": "Scopes",
|
||||
"options": "OAuth Scope"
|
||||
},
|
||||
{
|
||||
"fieldname": "authorization_uri",
|
||||
"fieldtype": "Data",
|
||||
"label": "Authorization URI"
|
||||
},
|
||||
{
|
||||
"fieldname": "token_uri",
|
||||
"fieldtype": "Data",
|
||||
"label": "Token URI"
|
||||
},
|
||||
{
|
||||
"fieldname": "revocation_uri",
|
||||
"fieldtype": "Data",
|
||||
"label": "Revocation URI"
|
||||
},
|
||||
{
|
||||
"fieldname": "userinfo_uri",
|
||||
"fieldtype": "Data",
|
||||
"label": "Userinfo URI"
|
||||
},
|
||||
{
|
||||
"fieldname": "introspection_uri",
|
||||
"fieldtype": "Data",
|
||||
"label": "Introspection URI"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_18",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Extra Parameters"
|
||||
},
|
||||
{
|
||||
"fieldname": "query_parameters",
|
||||
"fieldtype": "Table",
|
||||
"label": "Query Parameters",
|
||||
"options": "Query Parameters"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "Token Cache",
|
||||
"link_fieldname": "connected_app"
|
||||
}
|
||||
],
|
||||
"modified": "2020-11-16 16:29:50.277405",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Connected App",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "All"
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "provider_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
133
frappe/integrations/doctype/connected_app/connected_app.py
Normal file
133
frappe/integrations/doctype/connected_app/connected_app.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import os
|
||||
from urllib.parse import urljoin
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from requests_oauthlib import OAuth2Session
|
||||
|
||||
if any((os.getenv('CI'), frappe.conf.developer_mode, frappe.conf.allow_tests)):
|
||||
# Disable mandatory TLS in developer mode and tests
|
||||
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
|
||||
|
||||
class ConnectedApp(Document):
|
||||
"""Connect to a remote oAuth Server. Retrieve and store user's access token
|
||||
in a Token Cache.
|
||||
"""
|
||||
|
||||
def validate(self):
|
||||
base_url = frappe.utils.get_url()
|
||||
callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.name
|
||||
self.redirect_uri = urljoin(base_url, callback_path)
|
||||
|
||||
def get_oauth2_session(self, user=None, init=False):
|
||||
token = None
|
||||
token_updater = None
|
||||
|
||||
if not init:
|
||||
user = user or frappe.session.user
|
||||
token_cache = self.get_user_token(user)
|
||||
token = token_cache.get_json()
|
||||
token_updater = token_cache.update_data
|
||||
|
||||
return OAuth2Session(
|
||||
client_id=self.client_id,
|
||||
token=token,
|
||||
token_updater=token_updater,
|
||||
auto_refresh_url=self.token_uri,
|
||||
redirect_uri=self.redirect_uri,
|
||||
scope=self.get_scopes()
|
||||
)
|
||||
|
||||
def initiate_web_application_flow(self, user=None, success_uri=None):
|
||||
"""Return an authorization URL for the user. Save state in Token Cache."""
|
||||
user = user or frappe.session.user
|
||||
oauth = self.get_oauth2_session(init=True)
|
||||
query_params = self.get_query_params()
|
||||
authorization_url, state = oauth.authorization_url(self.authorization_uri, **query_params)
|
||||
token_cache = self.get_token_cache(user)
|
||||
|
||||
if not token_cache:
|
||||
token_cache = frappe.new_doc('Token Cache')
|
||||
token_cache.user = user
|
||||
token_cache.connected_app = self.name
|
||||
|
||||
token_cache.success_uri = success_uri
|
||||
token_cache.state = state
|
||||
token_cache.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
|
||||
return authorization_url
|
||||
|
||||
def get_user_token(self, user=None, success_uri=None):
|
||||
"""Return an existing user token or initiate a Web Application Flow."""
|
||||
user = user or frappe.session.user
|
||||
token_cache = self.get_token_cache(user)
|
||||
|
||||
if token_cache:
|
||||
return token_cache
|
||||
|
||||
redirect = self.initiate_web_application_flow(user, success_uri)
|
||||
frappe.local.response['type'] = 'redirect'
|
||||
frappe.local.response['location'] = redirect
|
||||
return redirect
|
||||
|
||||
def get_token_cache(self, user):
|
||||
token_cache = None
|
||||
token_cache_name = self.name + '-' + user
|
||||
|
||||
if frappe.db.exists('Token Cache', token_cache_name):
|
||||
token_cache = frappe.get_doc('Token Cache', token_cache_name)
|
||||
|
||||
return token_cache
|
||||
|
||||
def get_scopes(self):
|
||||
return [row.scope for row in self.scopes]
|
||||
|
||||
def get_query_params(self):
|
||||
return {param.key: param.value for param in self.query_parameters}
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def callback(code=None, state=None):
|
||||
"""Handle client's code.
|
||||
|
||||
Called during the oauthorization flow by the remote oAuth2 server to
|
||||
transmit a code that can be used by the local server to obtain an access
|
||||
token.
|
||||
"""
|
||||
if frappe.request.method != 'GET':
|
||||
frappe.throw(_('Invalid request method: {}').format(frappe.request.method))
|
||||
|
||||
if frappe.session.user == 'Guest':
|
||||
frappe.local.response['type'] = 'redirect'
|
||||
frappe.local.response['location'] = '/login?' + urlencode({'redirect-to': frappe.request.url})
|
||||
return
|
||||
|
||||
path = frappe.request.path[1:].split('/')
|
||||
if len(path) != 4 or not path[3]:
|
||||
frappe.throw(_('Invalid Parameters.'))
|
||||
|
||||
connected_app = frappe.get_doc('Connected App', path[3])
|
||||
token_cache = frappe.get_doc('Token Cache', connected_app.name + '-' + frappe.session.user)
|
||||
|
||||
if state != token_cache.state:
|
||||
frappe.throw(_('Invalid state.'))
|
||||
|
||||
oauth_session = connected_app.get_oauth2_session(init=True)
|
||||
query_params = connected_app.get_query_params()
|
||||
token = oauth_session.fetch_token(connected_app.token_uri,
|
||||
code=code,
|
||||
client_secret=connected_app.get_password('client_secret'),
|
||||
include_client_id=True,
|
||||
**query_params
|
||||
)
|
||||
token_cache.update_data(token)
|
||||
|
||||
frappe.local.response['type'] = 'redirect'
|
||||
frappe.local.response['location'] = token_cache.get('success_uri') or connected_app.get_url()
|
||||
162
frappe/integrations/doctype/connected_app/test_connected_app.py
Normal file
162
frappe/integrations/doctype/connected_app/test_connected_app.py
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest
|
||||
import requests
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import frappe
|
||||
from frappe.integrations.doctype.social_login_key.test_social_login_key import create_or_update_social_login_key
|
||||
|
||||
|
||||
def get_user(usr, pwd):
|
||||
user = frappe.new_doc('User')
|
||||
user.email = usr
|
||||
user.enabled = 1
|
||||
user.first_name = "_Test"
|
||||
user.new_password = pwd
|
||||
user.roles = []
|
||||
user.append('roles', {
|
||||
'doctype': 'Has Role',
|
||||
'parentfield': 'roles',
|
||||
'role': 'System Manager'
|
||||
})
|
||||
user.insert()
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def get_connected_app():
|
||||
doctype = 'Connected App'
|
||||
connected_app = frappe.new_doc(doctype)
|
||||
connected_app.provider_name = 'frappe'
|
||||
connected_app.scopes = []
|
||||
connected_app.append('scopes', {'scope': 'all'})
|
||||
connected_app.insert()
|
||||
|
||||
return connected_app
|
||||
|
||||
|
||||
def get_oauth_client():
|
||||
oauth_client = frappe.new_doc('OAuth Client')
|
||||
oauth_client.app_name = '_Test Connected App'
|
||||
oauth_client.redirect_uris = 'to be replaced'
|
||||
oauth_client.default_redirect_uri = 'to be replaced'
|
||||
oauth_client.grant_type = 'Authorization Code'
|
||||
oauth_client.response_type = 'Code'
|
||||
oauth_client.skip_authorization = 1
|
||||
oauth_client.insert()
|
||||
|
||||
return oauth_client
|
||||
|
||||
|
||||
class TestConnectedApp(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Set up a Connected App that connects to our own oAuth provider.
|
||||
|
||||
Frappe comes with it's own oAuth2 provider that we can test against. The
|
||||
client credentials can be obtained from an "OAuth Client". All depends
|
||||
on "Social Login Key" so we create one as well.
|
||||
|
||||
The redirect URIs from "Connected App" and "OAuth Client" have to match.
|
||||
Frappe's "Authorization URL" and "Access Token URL" (actually they're
|
||||
just endpoints) are stored in "Social Login Key" so we get them from
|
||||
there.
|
||||
"""
|
||||
self.user_name = 'test-connected-app@example.com'
|
||||
self.user_password = 'Eastern_43A1W'
|
||||
|
||||
self.user = get_user(self.user_name, self.user_password)
|
||||
self.connected_app = get_connected_app()
|
||||
self.oauth_client = get_oauth_client()
|
||||
social_login_key = create_or_update_social_login_key()
|
||||
self.base_url = social_login_key.get('base_url')
|
||||
|
||||
frappe.db.commit()
|
||||
self.connected_app.reload()
|
||||
self.oauth_client.reload()
|
||||
|
||||
redirect_uri = self.connected_app.get('redirect_uri')
|
||||
self.oauth_client.update({
|
||||
'redirect_uris': redirect_uri,
|
||||
'default_redirect_uri': redirect_uri
|
||||
})
|
||||
self.oauth_client.save()
|
||||
|
||||
self.connected_app.update({
|
||||
'authorization_uri': urljoin(self.base_url, social_login_key.get('authorize_url')),
|
||||
'client_id': self.oauth_client.get('client_id'),
|
||||
'client_secret': self.oauth_client.get('client_secret'),
|
||||
'token_uri': urljoin(self.base_url, social_login_key.get('access_token_url'))
|
||||
})
|
||||
self.connected_app.save()
|
||||
|
||||
frappe.db.commit()
|
||||
self.connected_app.reload()
|
||||
self.oauth_client.reload()
|
||||
|
||||
def test_web_application_flow(self):
|
||||
"""Simulate a logged in user who opens the authorization URL."""
|
||||
def login():
|
||||
return session.get(urljoin(self.base_url, '/api/method/login'), params={
|
||||
'usr': self.user_name,
|
||||
'pwd': self.user_password
|
||||
})
|
||||
|
||||
session = requests.Session()
|
||||
|
||||
# first login of a new user on a new site fails with "401 UNAUTHORIZED"
|
||||
# when anybody fixes that, the two lines below can be removed
|
||||
first_login = login()
|
||||
self.assertEqual(first_login.status_code, 401)
|
||||
|
||||
second_login = login()
|
||||
self.assertEqual(second_login.status_code, 200)
|
||||
|
||||
authorization_url = self.connected_app.initiate_web_application_flow(user=self.user_name)
|
||||
|
||||
auth_response = session.get(authorization_url)
|
||||
self.assertEqual(auth_response.status_code, 200)
|
||||
|
||||
callback_response = session.get(auth_response.url)
|
||||
self.assertEqual(callback_response.status_code, 200)
|
||||
|
||||
self.token_cache = self.connected_app.get_token_cache(self.user_name)
|
||||
token = self.token_cache.get_password('access_token')
|
||||
self.assertNotEqual(token, None)
|
||||
|
||||
oauth2_session = self.connected_app.get_oauth2_session(self.user_name)
|
||||
resp = oauth2_session.get(urljoin(self.base_url, '/api/method/frappe.auth.get_logged_user'))
|
||||
self.assertEqual(resp.json().get('message'), self.user_name)
|
||||
|
||||
def tearDown(self):
|
||||
def delete_if_exists(attribute):
|
||||
doc = getattr(self, attribute, None)
|
||||
if doc:
|
||||
doc.delete()
|
||||
|
||||
delete_if_exists('token_cache')
|
||||
delete_if_exists('connected_app')
|
||||
|
||||
if getattr(self, 'oauth_client', None):
|
||||
tokens = frappe.get_all('OAuth Bearer Token', filters={
|
||||
'client': self.oauth_client.name
|
||||
})
|
||||
for token in tokens:
|
||||
doc = frappe.get_doc('OAuth Bearer Token', token.name)
|
||||
doc.delete()
|
||||
|
||||
codes = frappe.get_all('OAuth Authorization Code', filters={
|
||||
'client': self.oauth_client.name
|
||||
})
|
||||
for code in codes:
|
||||
doc = frappe.get_doc('OAuth Authorization Code', code.name)
|
||||
doc.delete()
|
||||
|
||||
delete_if_exists('user')
|
||||
delete_if_exists('oauth_client')
|
||||
|
||||
frappe.db.commit()
|
||||
13
frappe/integrations/doctype/connected_app/test_records.json
Normal file
13
frappe/integrations/doctype/connected_app/test_records.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[
|
||||
{
|
||||
"doctype": "Connected App",
|
||||
"provider_name": "frappe",
|
||||
"client_id": "test_client_id",
|
||||
"client_secret": "test_client_secret",
|
||||
"scopes": [
|
||||
{
|
||||
"scope": "all"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
[
|
||||
{
|
||||
"app_name": "_Test OAuth Client",
|
||||
"client_id": "test_client_id",
|
||||
"app_name": "_Test OAuth Client",
|
||||
"client_secret": "test_client_secret",
|
||||
"default_redirect_uri": "http://localhost",
|
||||
"docstatus": 0,
|
||||
|
|
|
|||
0
frappe/integrations/doctype/oauth_scope/__init__.py
Normal file
0
frappe/integrations/doctype/oauth_scope/__init__.py
Normal file
30
frappe/integrations/doctype/oauth_scope/oauth_scope.json
Normal file
30
frappe/integrations/doctype/oauth_scope/oauth_scope.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2020-07-15 22:08:14.616585",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"scope"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "scope",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Scope"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-07-15 22:15:18.930632",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "OAuth Scope",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
10
frappe/integrations/doctype/oauth_scope/oauth_scope.py
Normal file
10
frappe/integrations/doctype/oauth_scope/oauth_scope.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, 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 OAuthScope(Document):
|
||||
pass
|
||||
0
frappe/integrations/doctype/query_parameters/__init__.py
Normal file
0
frappe/integrations/doctype/query_parameters/__init__.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2020-11-16 14:54:37.226914",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"key",
|
||||
"value"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "key",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Key",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "value",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Value",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-16 15:18:35.887149",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Query Parameters",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, 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 QueryParameters(Document):
|
||||
pass
|
||||
|
|
@ -22,3 +22,17 @@ def make_social_login_key(**kwargs):
|
|||
kwargs["provider_name"] = "Test OAuth2 Provider"
|
||||
doc = frappe.get_doc(kwargs)
|
||||
return doc
|
||||
|
||||
def create_or_update_social_login_key():
|
||||
# used in other tests (connected app, oauth20)
|
||||
try:
|
||||
social_login_key = frappe.get_doc("Social Login Key", "frappe")
|
||||
except frappe.DoesNotExistError:
|
||||
social_login_key = frappe.new_doc("Social Login Key")
|
||||
social_login_key.get_social_login_provider("Frappe", initialize=True)
|
||||
social_login_key.base_url = frappe.utils.get_url()
|
||||
social_login_key.enable_social_login = 0
|
||||
social_login_key.save()
|
||||
frappe.db.commit()
|
||||
|
||||
return social_login_key
|
||||
|
|
|
|||
0
frappe/integrations/doctype/token_cache/__init__.py
Normal file
0
frappe/integrations/doctype/token_cache/__init__.py
Normal file
18
frappe/integrations/doctype/token_cache/test_records.json
Normal file
18
frappe/integrations/doctype/token_cache/test_records.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[
|
||||
{
|
||||
"doctype": "Token Cache",
|
||||
"user": "test@example.com",
|
||||
"access_token": "test-access-token",
|
||||
"refresh_token": "test-refresh-token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 1000,
|
||||
"scopes": [
|
||||
{
|
||||
"scope": "all"
|
||||
},
|
||||
{
|
||||
"scope": "openid"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
37
frappe/integrations/doctype/token_cache/test_token_cache.py
Normal file
37
frappe/integrations/doctype/token_cache/test_token_cache.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest
|
||||
import frappe
|
||||
|
||||
test_dependencies = ['User', 'Connected App', 'Token Cache']
|
||||
|
||||
class TestTokenCache(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.token_cache = frappe.get_last_doc('Token Cache')
|
||||
self.token_cache.update({'connected_app': frappe.get_last_doc('Connected App').name})
|
||||
self.token_cache.save()
|
||||
|
||||
def test_get_auth_header(self):
|
||||
self.token_cache.get_auth_header()
|
||||
|
||||
def test_update_data(self):
|
||||
self.token_cache.update_data({
|
||||
'access_token': 'new-access-token',
|
||||
'refresh_token': 'new-refresh-token',
|
||||
'token_type': 'bearer',
|
||||
'expires_in': 2000,
|
||||
'scope': 'new scope'
|
||||
})
|
||||
|
||||
def test_get_expires_in(self):
|
||||
self.token_cache.get_expires_in()
|
||||
|
||||
def test_is_expired(self):
|
||||
self.token_cache.is_expired()
|
||||
|
||||
def get_json(self):
|
||||
self.token_cache.get_json()
|
||||
8
frappe/integrations/doctype/token_cache/token_cache.js
Normal file
8
frappe/integrations/doctype/token_cache/token_cache.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2019, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Token Cache', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
110
frappe/integrations/doctype/token_cache/token_cache.json
Normal file
110
frappe/integrations/doctype/token_cache/token_cache.json
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "format:{connected_app}-{user}",
|
||||
"beta": 1,
|
||||
"creation": "2019-01-24 16:56:55.631096",
|
||||
"doctype": "DocType",
|
||||
"document_type": "System",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"user",
|
||||
"connected_app",
|
||||
"provider_name",
|
||||
"access_token",
|
||||
"refresh_token",
|
||||
"expires_in",
|
||||
"state",
|
||||
"scopes",
|
||||
"success_uri",
|
||||
"token_type"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "connected_app",
|
||||
"fieldtype": "Link",
|
||||
"label": "Connected App",
|
||||
"options": "Connected App",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "access_token",
|
||||
"fieldtype": "Password",
|
||||
"label": "Access Token",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "refresh_token",
|
||||
"fieldtype": "Password",
|
||||
"label": "Refresh Token",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "expires_in",
|
||||
"fieldtype": "Int",
|
||||
"label": "Expires In",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "scopes",
|
||||
"fieldtype": "Table",
|
||||
"label": "Scopes",
|
||||
"options": "OAuth Scope",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "success_uri",
|
||||
"fieldtype": "Data",
|
||||
"label": "Success URI",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "token_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Token Type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "connected_app.provider_name",
|
||||
"fieldname": "provider_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Provider Name",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-11-13 13:35:53.714352",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Token Cache",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"delete": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager"
|
||||
},
|
||||
{
|
||||
"delete": 1,
|
||||
"if_owner": 1,
|
||||
"read": 1,
|
||||
"role": "All"
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
67
frappe/integrations/doctype/token_cache/token_cache.py
Normal file
67
frappe/integrations/doctype/token_cache/token_cache.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cstr, cint
|
||||
from frappe.model.document import Document
|
||||
|
||||
class TokenCache(Document):
|
||||
|
||||
def get_auth_header(self):
|
||||
if self.access_token:
|
||||
headers = {'Authorization': 'Bearer ' + self.get_password('access_token')}
|
||||
return headers
|
||||
|
||||
raise frappe.exceptions.DoesNotExistError
|
||||
|
||||
def update_data(self, data):
|
||||
"""
|
||||
Store data returned by authorization flow.
|
||||
|
||||
Params:
|
||||
data - Dict with access_token, refresh_token, expires_in and scope.
|
||||
"""
|
||||
token_type = cstr(data.get('token_type', '')).lower()
|
||||
if token_type not in ['bearer', 'mac']:
|
||||
frappe.throw(_('Received an invalid token type.'))
|
||||
# 'Bearer' or 'MAC'
|
||||
token_type = token_type.title() if token_type == 'bearer' else token_type.upper()
|
||||
|
||||
self.token_type = token_type
|
||||
self.access_token = cstr(data.get('access_token', ''))
|
||||
self.refresh_token = cstr(data.get('refresh_token', ''))
|
||||
self.expires_in = cint(data.get('expires_in', 0))
|
||||
|
||||
new_scopes = data.get('scope')
|
||||
if new_scopes:
|
||||
if isinstance(new_scopes, str):
|
||||
new_scopes = new_scopes.split(' ')
|
||||
if isinstance(new_scopes, list):
|
||||
self.scopes = None
|
||||
for scope in new_scopes:
|
||||
self.append('scopes', {'scope': scope})
|
||||
|
||||
self.state = None
|
||||
self.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
return self
|
||||
|
||||
def get_expires_in(self):
|
||||
expiry_time = frappe.utils.get_datetime(self.modified) + timedelta(self.expires_in)
|
||||
return (datetime.now() - expiry_time).total_seconds()
|
||||
|
||||
def is_expired(self):
|
||||
return self.get_expires_in() < 0
|
||||
|
||||
def get_json(self):
|
||||
return {
|
||||
'access_token': self.get_password('access_token', ''),
|
||||
'refresh_token': self.get_password('refresh_token', ''),
|
||||
'expires_in': self.get_expires_in(),
|
||||
'token_type': self.token_type
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ def get_oauth_server():
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -484,6 +484,8 @@ class Meta(Document):
|
|||
if not data.transactions:
|
||||
# init groups
|
||||
data.transactions = []
|
||||
|
||||
if not data.non_standard_fieldnames:
|
||||
data.non_standard_fieldnames = {}
|
||||
|
||||
for link in dashboard_links:
|
||||
|
|
|
|||
|
|
@ -21,8 +21,16 @@ def update_document_title(doctype, docname, title_field=None, old_title=None, ne
|
|||
docname = rename_doc(doctype=doctype, old=docname, new=new_name, merge=merge)
|
||||
|
||||
if old_title and new_title and not old_title == new_title:
|
||||
frappe.db.set_value(doctype, docname, title_field, new_title)
|
||||
frappe.msgprint(_('Saved'), alert=True, indicator='green')
|
||||
try:
|
||||
frappe.db.set_value(doctype, docname, title_field, new_title)
|
||||
frappe.msgprint(_('Saved'), alert=True, indicator='green')
|
||||
except Exception as e:
|
||||
if frappe.db.is_duplicate_entry(e):
|
||||
frappe.throw(
|
||||
_("{0} {1} already exists").format(doctype, frappe.bold(docname)),
|
||||
title=_("Duplicate Name"),
|
||||
exc=frappe.DuplicateEntryError
|
||||
)
|
||||
|
||||
return docname
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,23 @@ import frappe
|
|||
|
||||
def execute():
|
||||
frappe.reload_doctype('Website Theme')
|
||||
frappe.reload_doc('website', 'doctype', 'website_theme_ignore_app')
|
||||
frappe.reload_doc('website', 'doctype', 'color')
|
||||
|
||||
for theme in frappe.get_all('Website Theme'):
|
||||
doc = frappe.get_doc('Website Theme', theme.name)
|
||||
if not doc.get('custom_scss') and doc.theme_scss:
|
||||
# move old theme to new theme
|
||||
doc.custom_scss = doc.theme_scss
|
||||
|
||||
if doc.background_color:
|
||||
setup_color_record(doc.background_color)
|
||||
|
||||
doc.save()
|
||||
|
||||
def setup_color_record(color):
|
||||
frappe.get_doc({
|
||||
"doctype": "Color",
|
||||
"__newname": color,
|
||||
"color": color,
|
||||
}).save()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
|
|||
.appendTo(this.input_area);
|
||||
|
||||
this.expanded = false;
|
||||
this.$expand_button = $(`<button class="btn btn-xs btn-default">${__('Expand')}</button>`).click(() => {
|
||||
this.$expand_button = $(`<button class="btn btn-xs btn-default">${this.get_button_label()}</button>`).click(() => {
|
||||
this.expanded = !this.expanded;
|
||||
this.refresh_height();
|
||||
this.toggle_label();
|
||||
|
|
@ -38,8 +38,11 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
|
|||
},
|
||||
|
||||
toggle_label() {
|
||||
const button_label = this.expanded ? __('Collapse') : __('Expand');
|
||||
this.$expand_button && this.$expand_button.text(button_label);
|
||||
this.$expand_button && this.$expand_button.text(this.get_button_label());
|
||||
},
|
||||
|
||||
get_button_label() {
|
||||
return this.expanded ? __('Collapse', null, 'Shrink code field.') : __('Expand', null, 'Enlarge code field.');
|
||||
},
|
||||
|
||||
set_language() {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({
|
|||
this.map_area.prependTo($input_wrapper);
|
||||
this.$wrapper.find('.control-input').addClass("hidden");
|
||||
|
||||
if ($input_wrapper.is(':visible')) {
|
||||
if (this.frm) {
|
||||
this.make_map();
|
||||
} else {
|
||||
$(document).on('frappe.ui.Dialog:shown', () => {
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ frappe.form.formatters = {
|
|||
} else if(docfield && doctype) {
|
||||
if (!frappe.model.can_select(doctype) && frappe.model.can_read(doctype)) {
|
||||
return `<a class="grey"
|
||||
#Form/${encodeURIComponent(doctype)}/${encodeURIComponent(original_value)}
|
||||
href="#Form/${encodeURIComponent(doctype)}/${encodeURIComponent(original_value)}"
|
||||
data-doctype="${doctype}"
|
||||
data-name="${original_value}">
|
||||
${__(options && options.label || value)}</a>`;
|
||||
|
|
|
|||
|
|
@ -568,13 +568,15 @@ export default class GridRow {
|
|||
this.wrapper.removeClass("grid-row-open");
|
||||
}
|
||||
open_prev() {
|
||||
if(this.grid.grid_rows[this.doc.idx-2]) {
|
||||
this.grid.grid_rows[this.doc.idx-2].toggle_view(true);
|
||||
const row_index = this.wrapper.index();
|
||||
if (this.grid.grid_rows[row_index - 1]) {
|
||||
this.grid.grid_rows[row_index - 1].toggle_view(true);
|
||||
}
|
||||
}
|
||||
open_next() {
|
||||
if(this.grid.grid_rows[this.doc.idx]) {
|
||||
this.grid.grid_rows[this.doc.idx].toggle_view(true);
|
||||
const row_index = this.wrapper.index();
|
||||
if (this.grid.grid_rows[row_index + 1]) {
|
||||
this.grid.grid_rows[row_index + 1].toggle_view(true);
|
||||
} else {
|
||||
this.grid.add_new_row(null, null, true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,17 +16,19 @@ window.refresh_field = function(n, docname, table_field) {
|
|||
if(typeof n==typeof [])
|
||||
refresh_many(n, docname, table_field);
|
||||
|
||||
if (n && typeof n==='string' && table_field){
|
||||
if (n && typeof n==='string' && table_field) {
|
||||
var grid = cur_frm.fields_dict[table_field].grid,
|
||||
field = frappe.utils.filter_dict(grid.docfields, {fieldname: n});
|
||||
if (field && field.length){
|
||||
field = frappe.utils.filter_dict(grid.docfields, {fieldname: n}),
|
||||
grid_row = grid.grid_rows_by_docname[docname];
|
||||
|
||||
if (field && field.length) {
|
||||
field = field[0];
|
||||
var meta = frappe.meta.get_docfield(field.parent, field.fieldname, docname);
|
||||
$.extend(field, meta);
|
||||
if (docname){
|
||||
cur_frm.fields_dict[table_field].grid.grid_rows_by_docname[docname].refresh_field(n);
|
||||
if (grid_row) {
|
||||
grid_row.refresh_field(n);
|
||||
} else {
|
||||
cur_frm.fields_dict[table_field].grid.refresh();
|
||||
grid.refresh();
|
||||
}
|
||||
}
|
||||
} else if(cur_frm) {
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ frappe.ui.form.Sidebar = class {
|
|||
__("{0} edited this {1}", [
|
||||
frappe.user.full_name(this.frm.doc.modified_by).bold(),
|
||||
"<br>" + comment_when(this.frm.doc.modified),
|
||||
])
|
||||
], "For example, 'Jon Doe edited this 5 minutes ago'.")
|
||||
);
|
||||
this.sidebar
|
||||
.find(".created-by")
|
||||
|
|
@ -107,7 +107,7 @@ frappe.ui.form.Sidebar = class {
|
|||
__("{0} created this {1}", [
|
||||
frappe.user.full_name(this.frm.doc.owner).bold(),
|
||||
"<br>" + comment_when(this.frm.doc.creation),
|
||||
])
|
||||
], "For example, 'Jon Doe created this 5 minutes ago'.")
|
||||
);
|
||||
|
||||
this.refresh_like();
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ $.extend(frappe.user, {
|
|||
name: 'Guest',
|
||||
full_name: function(uid) {
|
||||
return uid === frappe.session.user ?
|
||||
__("You") :
|
||||
__("You", null, "Name of the current user. For example: You edited this 5 hours ago.") :
|
||||
frappe.user_info(uid).fullname;
|
||||
},
|
||||
image: function(uid) {
|
||||
|
|
|
|||
|
|
@ -672,10 +672,19 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
}
|
||||
},
|
||||
|
||||
setup_earlier_reply: function() {
|
||||
get_default_outgoing_email_account_signature: function() {
|
||||
return frappe.db.get_value('Email Account', { 'default_outgoing': 1, 'add_signature': 1 }, 'signature');
|
||||
},
|
||||
|
||||
setup_earlier_reply: async function() {
|
||||
let fields = this.dialog.fields_dict;
|
||||
let signature = frappe.boot.user.email_signature || "";
|
||||
|
||||
if (!signature) {
|
||||
const res = await this.get_default_outgoing_email_account_signature();
|
||||
signature = res.message.signature;
|
||||
}
|
||||
|
||||
if(!frappe.utils.is_html(signature)) {
|
||||
signature = signature.replace(/\n/g, "<br>");
|
||||
}
|
||||
|
|
@ -756,4 +765,3 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
return text.replace(/\n{3,}/g, '\n\n');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -444,7 +444,7 @@ export default class OnboardingWidget extends Widget {
|
|||
set_actions() {
|
||||
this.action_area.empty();
|
||||
const dismiss = $(
|
||||
`<div class="small" style="cursor:pointer;">${__('Dismiss')}</div>`
|
||||
`<div class="small" style="cursor:pointer;">${__('Dismiss', null, 'Stop showing the onboarding widget.')}</div>`
|
||||
);
|
||||
dismiss.on("click", () => {
|
||||
let dismissed = JSON.parse(
|
||||
|
|
|
|||
|
|
@ -29,11 +29,11 @@
|
|||
}
|
||||
|
||||
.hero.align-center {
|
||||
h1, .hero-subtitle, .hero-buttons {
|
||||
h1, .hero-title, .hero-subtitle, .hero-buttons {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
.hero-title, .hero-subtitle {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@
|
|||
<div>{{ frappe.render_template(df.options, {"doc": doc}) or "" }}</div>
|
||||
{%- elif df.fieldtype in ("Text", "Text Editor", "Code", "Long Text") -%}
|
||||
{{ render_text_field(df, doc) }}
|
||||
{%- elif df.fieldtype in ("Image", "Attach Image", "Attach")
|
||||
and (guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/") -%}
|
||||
{%- elif df.fieldtype in ("Image", "Attach Image")
|
||||
and (
|
||||
(guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/")
|
||||
or doc[df.fieldname].startswith("http")
|
||||
) -%}
|
||||
{{ render_image(df, doc) }}
|
||||
{%- elif df.fieldtype=="Geolocation" -%}
|
||||
{{ render_geolocation(df, doc) }}
|
||||
|
|
@ -123,15 +126,14 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
|
|||
{% include doc.print_templates[df.fieldname] %}
|
||||
{% elif df.fieldtype=="Check" %}
|
||||
<i class="{{ 'fa fa-check' if doc[df.fieldname] }}"></i>
|
||||
{% elif df.fieldtype=="Image" %}
|
||||
{% elif df.fieldtype in ("Image", "Attach Image") %}
|
||||
<img src="{{ doc[doc.meta.get_field(df.fieldname).options] }}"
|
||||
class="img-responsive"
|
||||
{%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}>
|
||||
{% elif df.fieldtype=="Signature" %}
|
||||
<img src="{{ doc[df.fieldname] }}" class="signature-img img-responsive"
|
||||
{%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}>
|
||||
{% elif df.fieldtype in ("Attach", "Attach Image") and doc[df.fieldname]
|
||||
and frappe.utils.is_image(doc[df.fieldname]) %}
|
||||
{% elif df.fieldtype == "Attach" and doc[df.fieldname] and frappe.utils.is_image(doc[df.fieldname]) %}
|
||||
<img src="{{ doc[df.fieldname] }}" class="img-responsive"
|
||||
{%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}>
|
||||
{% elif df.fieldtype=="HTML" %}
|
||||
|
|
|
|||
|
|
@ -154,14 +154,22 @@ def get_time_zone():
|
|||
|
||||
return frappe.cache().get_value("time_zone", _get_time_zone)
|
||||
|
||||
def convert_utc_to_user_timezone(utc_timestamp):
|
||||
def convert_utc_to_timezone(utc_timestamp, time_zone):
|
||||
from pytz import timezone, UnknownTimeZoneError
|
||||
utcnow = timezone('UTC').localize(utc_timestamp)
|
||||
try:
|
||||
return utcnow.astimezone(timezone(get_time_zone()))
|
||||
return utcnow.astimezone(timezone(time_zone))
|
||||
except UnknownTimeZoneError:
|
||||
return utcnow
|
||||
|
||||
def get_datetime_in_timezone(time_zone):
|
||||
utc_timestamp = datetime.datetime.utcnow()
|
||||
return convert_utc_to_timezone(utc_timestamp, time_zone)
|
||||
|
||||
def convert_utc_to_user_timezone(utc_timestamp):
|
||||
time_zone = get_time_zone()
|
||||
return convert_utc_to_timezone(utc_timestamp, time_zone)
|
||||
|
||||
def now():
|
||||
"""return current datetime as yyyy-mm-dd hh:mm:ss"""
|
||||
if frappe.flags.current_date:
|
||||
|
|
@ -369,7 +377,7 @@ def format_duration(seconds, hide_days=False):
|
|||
|
||||
example: converts 12885 to '3h 34m 45s' where 12885 = seconds in float
|
||||
"""
|
||||
|
||||
|
||||
seconds = cint(seconds)
|
||||
|
||||
total_duration = {
|
||||
|
|
|
|||
|
|
@ -230,12 +230,19 @@ def update_oauth_user(user, data, provider):
|
|||
|
||||
save = True
|
||||
user = frappe.new_doc("User")
|
||||
|
||||
gender = (data.get("gender") or "").title()
|
||||
|
||||
if not frappe.db.exists("Gender", gender):
|
||||
doc = frappe.new_doc("Gender", {"gender": gender})
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
user.update({
|
||||
"doctype":"User",
|
||||
"first_name": get_first_name(data),
|
||||
"last_name": get_last_name(data),
|
||||
"email": get_email(data),
|
||||
"gender": (data.get("gender") or "").title(),
|
||||
"gender": gender,
|
||||
"enabled": 1,
|
||||
"new_password": frappe.generate_hash(get_email(data)),
|
||||
"location": data.get("location"),
|
||||
|
|
@ -306,7 +313,7 @@ def redirect_post_login(desk_user, redirect_to=None, provider=None):
|
|||
frappe.local.response["type"] = "redirect"
|
||||
|
||||
if not redirect_to:
|
||||
# the #desktop is added to prevent a facebook redirect bug
|
||||
# the #workspace is added to prevent a facebook redirect bug
|
||||
desk_uri = "/desk#workspace" if provider == 'facebook' else '/desk'
|
||||
redirect_to = desk_uri if desk_user else "/me"
|
||||
redirect_to = frappe.utils.get_url(redirect_to)
|
||||
|
|
|
|||
|
|
@ -222,6 +222,7 @@ VALID_UTILS = (
|
|||
"get_last_day_of_week",
|
||||
"get_last_day",
|
||||
"get_time",
|
||||
"get_datetime_in_timezone",
|
||||
"get_datetime_str",
|
||||
"get_date_str",
|
||||
"get_time_str",
|
||||
|
|
|
|||
|
|
@ -65,8 +65,10 @@
|
|||
},
|
||||
{
|
||||
"fieldname": "theme_url",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Theme URL"
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Theme URL",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
|
|
@ -179,7 +181,7 @@
|
|||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-24 11:42:33.867840",
|
||||
"modified": "2021-01-18 17:43:39.804765",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Website Theme",
|
||||
|
|
|
|||
6
frappe/website/js/bootstrap-4.js
vendored
6
frappe/website/js/bootstrap-4.js
vendored
|
|
@ -18,7 +18,7 @@ $('.dropdown-menu a.dropdown-toggle').on('click', function (e) {
|
|||
return false;
|
||||
});
|
||||
|
||||
frappe.get_modal = function(title, content) {
|
||||
frappe.get_modal = function (title, content) {
|
||||
return $(
|
||||
`<div class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
|
|
@ -33,6 +33,10 @@ frappe.get_modal = function(title, content) {
|
|||
${content}
|
||||
</div>
|
||||
<div class="modal-footer hidden">
|
||||
<button type="button" class="btn btn-default btn-sm btn-modal-close" data-dismiss="modal">
|
||||
<i class="octicon octicon-x visible-xs" style="padding: 1px 0px;"></i>
|
||||
<span class="hidden-xs">${__("Close")}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary hidden"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
<div class="testimonial-content">
|
||||
<span>“</span>
|
||||
{{ content }}
|
||||
<span>”</span>
|
||||
“{{ content }}”
|
||||
</div>
|
||||
<div class="testimonial-by">
|
||||
{{ name }}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ pypng==0.0.20
|
|||
PyQRCode==1.2.1
|
||||
python-dateutil==2.8.1
|
||||
pytz==2019.3
|
||||
PyYAML==5.3.1
|
||||
PyYAML==5.4
|
||||
rauth==0.7.3
|
||||
redis==3.5.3
|
||||
requests-oauthlib==1.3.0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue