Merge branch 'develop' into multiple_imap_folder
This commit is contained in:
commit
c3f78c4770
78 changed files with 689 additions and 4767 deletions
2
.github/workflows/docker-release.yml
vendored
2
.github/workflows/docker-release.yml
vendored
|
|
@ -12,4 +12,4 @@ jobs:
|
|||
- name: curl
|
||||
run: |
|
||||
apk add curl bash
|
||||
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token ${{ secrets.TRAVIS_CI_TOKEN }}" -d '{"request":{"branch":"master"}}' https://api.travis-ci.com/repo/frappe%2Ffrappe_docker/requests
|
||||
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer ${{ secrets.CI_PAT }}" https://api.github.com/repos/frappe/frappe_docker/actions/workflows/build_stable.yml/dispatches -d '{"ref":"main"}'
|
||||
|
|
|
|||
|
|
@ -30,11 +30,6 @@ export default {
|
|||
"link_doctype": "Contact",
|
||||
"link_fieldname": "user"
|
||||
},
|
||||
{
|
||||
"group": "Profile",
|
||||
"link_doctype": "Chat Profile",
|
||||
"link_fieldname": "user"
|
||||
},
|
||||
],
|
||||
modified_by: 'Administrator',
|
||||
module: 'Custom',
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ class _dict(dict):
|
|||
"""dict like object that exposes keys as attributes"""
|
||||
def __getattr__(self, key):
|
||||
ret = self.get(key)
|
||||
if not ret and key.startswith("__"):
|
||||
# "__deepcopy__" exception added to fix frappe#14833 via DFP
|
||||
if not ret and key.startswith("__") and key != "__deepcopy__":
|
||||
raise AttributeError()
|
||||
return ret
|
||||
def __setattr__(self, key, value):
|
||||
|
|
@ -1797,7 +1798,7 @@ def get_version(doctype, name, limit=None, head=False, raise_err=True):
|
|||
'limit': limit
|
||||
}, as_list=1)
|
||||
|
||||
from frappe.chat.util import squashify, dictify, safe_json_loads
|
||||
from frappe.utils import squashify, dictify, safe_json_loads
|
||||
|
||||
versions = []
|
||||
|
||||
|
|
@ -1855,7 +1856,7 @@ def mock(type, size=1, locale='en'):
|
|||
data = getattr(fake, type)()
|
||||
results.append(data)
|
||||
|
||||
from frappe.chat.util import squashify
|
||||
from frappe.utils import squashify
|
||||
return squashify(results)
|
||||
|
||||
def validate_and_sanitize_search_inputs(fn):
|
||||
|
|
|
|||
|
|
@ -128,7 +128,6 @@ class LoginManager:
|
|||
self.make_session()
|
||||
self.set_user_info()
|
||||
|
||||
@frappe.whitelist()
|
||||
def login(self):
|
||||
# clear cache
|
||||
frappe.clear_cache(user = frappe.form_dict.get('usr'))
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
session = frappe.session
|
||||
|
||||
def authenticate(user, raise_err = True):
|
||||
if session.user == 'Guest':
|
||||
if not frappe.db.exists('Chat Token', user):
|
||||
if raise_err:
|
||||
frappe.throw(_("Sorry, you're not authorized."))
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
if user != session.user:
|
||||
if raise_err:
|
||||
frappe.throw(_("Sorry, you're not authorized."))
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
// Copyright (c) 2017, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Chat Message', {
|
||||
onload: function(frm) {
|
||||
if(frm.doc.type == 'File') {
|
||||
frm.set_df_property('content', 'read_only', 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
{
|
||||
"beta": 1,
|
||||
"creation": "2017-11-10 11:10:40.011099",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"room_type",
|
||||
"type",
|
||||
"user",
|
||||
"room",
|
||||
"content",
|
||||
"mentions",
|
||||
"urls"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "room_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Room Type",
|
||||
"options": "Direct\nGroup\nVisitor",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Type",
|
||||
"options": "Content\nFile"
|
||||
},
|
||||
{
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "room",
|
||||
"fieldtype": "Link",
|
||||
"label": "Room",
|
||||
"options": "Chat Room",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "content",
|
||||
"fieldtype": "Text",
|
||||
"label": "Content",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "mentions",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Mentions"
|
||||
},
|
||||
{
|
||||
"fieldname": "urls",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "URLs"
|
||||
}
|
||||
],
|
||||
"modified": "2020-09-18 17:26:09.703215",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Chat",
|
||||
"name": "Chat Message",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"search_fields": "content, user",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "content",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
# imports - standard imports
|
||||
import json
|
||||
|
||||
# imports - third-party imports
|
||||
import requests
|
||||
from bs4 import BeautifulSoup as Soup
|
||||
|
||||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
from frappe import _, _dict
|
||||
import frappe
|
||||
|
||||
# imports - frappe module imports
|
||||
from frappe.chat import authenticate
|
||||
from frappe.chat.util import (
|
||||
get_if_empty,
|
||||
check_url,
|
||||
dictify,
|
||||
get_emojis,
|
||||
safe_json_loads,
|
||||
get_user_doc,
|
||||
squashify
|
||||
)
|
||||
|
||||
session = frappe.session
|
||||
|
||||
class ChatMessage(Document):
|
||||
pass
|
||||
|
||||
def get_message_urls(content):
|
||||
soup = Soup(content, 'html.parser')
|
||||
anchors = soup.find_all('a')
|
||||
urls = [ ]
|
||||
|
||||
for anchor in anchors:
|
||||
text = anchor.text
|
||||
|
||||
if check_url(text):
|
||||
urls.append(text)
|
||||
|
||||
return urls
|
||||
|
||||
def get_message_mentions(content):
|
||||
mentions = [ ]
|
||||
tokens = content.split(' ')
|
||||
|
||||
for token in tokens:
|
||||
if token.startswith('@'):
|
||||
what = token[1:]
|
||||
if frappe.db.exists('User', what):
|
||||
mentions.append(what)
|
||||
else:
|
||||
if frappe.db.exists('User', token):
|
||||
mentions.append(token)
|
||||
|
||||
return mentions
|
||||
|
||||
def get_message_meta(content):
|
||||
'''
|
||||
Assumes content to be HTML. Sanitizes the content
|
||||
into a dict of metadata values.
|
||||
'''
|
||||
meta = _dict(
|
||||
links = [ ],
|
||||
mentions = [ ]
|
||||
)
|
||||
|
||||
meta.content = content
|
||||
meta.urls = get_message_urls(content)
|
||||
meta.mentions = get_message_mentions(content)
|
||||
|
||||
return meta
|
||||
|
||||
def sanitize_message_content(content):
|
||||
emojis = get_emojis()
|
||||
|
||||
tokens = content.split(' ')
|
||||
for token in tokens:
|
||||
if token.startswith(':') and token.endswith(':'):
|
||||
what = token[1:-1]
|
||||
|
||||
# Expensive, I know.
|
||||
for emoji in emojis:
|
||||
for alias in emoji.aliases:
|
||||
if what == alias:
|
||||
content = content.replace(token, emoji.emoji)
|
||||
|
||||
return content
|
||||
|
||||
def get_new_chat_message_doc(user, room, content, type = "Content", link = True):
|
||||
user = get_user_doc(user)
|
||||
room = frappe.get_doc('Chat Room', room)
|
||||
|
||||
meta = get_message_meta(content)
|
||||
mess = frappe.new_doc('Chat Message')
|
||||
mess.room = room.name
|
||||
mess.room_type = room.type
|
||||
mess.content = sanitize_message_content(content)
|
||||
mess.type = type
|
||||
mess.user = user.name
|
||||
|
||||
mess.mentions = json.dumps(meta.mentions)
|
||||
mess.urls = ','.join(meta.urls)
|
||||
mess.save(ignore_permissions = True)
|
||||
|
||||
if link:
|
||||
room.update(dict(
|
||||
last_message = mess.name
|
||||
))
|
||||
room.save(ignore_permissions = True)
|
||||
|
||||
return mess
|
||||
|
||||
def get_new_chat_message(user, room, content, type = "Content"):
|
||||
mess = get_new_chat_message_doc(user, room, content, type)
|
||||
|
||||
resp = dict(
|
||||
name = mess.name,
|
||||
user = mess.user,
|
||||
room = mess.room,
|
||||
room_type = mess.room_type,
|
||||
content = json.loads(mess.content) if mess.type in ["File"] else mess.content,
|
||||
urls = mess.urls,
|
||||
mentions = json.loads(mess.mentions),
|
||||
creation = mess.creation,
|
||||
seen = json.loads(mess._seen) if mess._seen else [ ],
|
||||
)
|
||||
|
||||
return resp
|
||||
|
||||
@frappe.whitelist(allow_guest = True)
|
||||
def send(user, room, content, type = "Content"):
|
||||
mess = get_new_chat_message(user, room, content, type)
|
||||
|
||||
frappe.publish_realtime('frappe.chat.message:create', mess, room = room,
|
||||
after_commit = True)
|
||||
|
||||
@frappe.whitelist(allow_guest = True)
|
||||
def seen(message, user = None):
|
||||
authenticate(user)
|
||||
|
||||
has_message = frappe.db.exists('Chat Message', message)
|
||||
|
||||
if has_message:
|
||||
mess = frappe.get_doc('Chat Message', message)
|
||||
mess.add_seen(user)
|
||||
mess.load_from_db()
|
||||
room = mess.room
|
||||
resp = dict(message = message, data = dict(seen = json.loads(mess._seen) if mess._seen else []))
|
||||
|
||||
frappe.publish_realtime('frappe.chat.message:update', resp, room = room, after_commit = True)
|
||||
|
||||
def history(room, fields = None, limit = 10, start = None, end = None):
|
||||
room = frappe.get_doc('Chat Room', room)
|
||||
mess = frappe.get_all('Chat Message',
|
||||
filters = [
|
||||
('Chat Message', 'room', '=', room.name),
|
||||
('Chat Message', 'room_type', '=', room.type)
|
||||
],
|
||||
fields = fields if fields else [
|
||||
'name', 'room_type', 'room', 'content', 'type', 'user', 'mentions', 'urls', 'creation', '_seen'
|
||||
],
|
||||
order_by = 'creation'
|
||||
)
|
||||
|
||||
if not fields or 'seen' in fields:
|
||||
for m in mess:
|
||||
m['seen'] = json.loads(m._seen) if m._seen else [ ]
|
||||
del m['_seen']
|
||||
if not fields or 'content' in fields:
|
||||
for m in mess:
|
||||
m['content'] = json.loads(m.content) if m.type in ["File"] else m.content
|
||||
|
||||
frappe.enqueue('frappe.chat.doctype.chat_message.chat_message.mark_messages_as_seen',
|
||||
message_names=[m.name for m in mess], user=frappe.session.user)
|
||||
|
||||
return mess
|
||||
|
||||
def mark_messages_as_seen(message_names, user):
|
||||
'''
|
||||
Marks chat messages as seen, updates the _seen for each message
|
||||
(should be run in background process)
|
||||
'''
|
||||
for name in message_names:
|
||||
seen = frappe.db.get_value('Chat Message', name, '_seen') or '[]'
|
||||
seen = json.loads(seen)
|
||||
seen.append(user)
|
||||
seen = json.dumps(seen)
|
||||
frappe.db.set_value('Chat Message', name, '_seen', seen, update_modified=False)
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get(name, rooms = None, fields = None):
|
||||
rooms, fields = safe_json_loads(rooms, fields)
|
||||
|
||||
has_message = frappe.db.exists('Chat Message', name)
|
||||
|
||||
if has_message:
|
||||
dmess = frappe.get_doc('Chat Message', name)
|
||||
data = dict(
|
||||
name = dmess.name,
|
||||
user = dmess.user,
|
||||
room = dmess.room,
|
||||
room_type = dmess.room_type,
|
||||
content = json.loads(dmess.content) if dmess.type in ["File"] else dmess.content,
|
||||
type = dmess.type,
|
||||
urls = dmess.urls,
|
||||
mentions = dmess.mentions,
|
||||
creation = dmess.creation,
|
||||
seen = get_if_empty(dmess._seen, [ ])
|
||||
)
|
||||
|
||||
return data
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
frappe.listview_settings['Chat Message'] = {
|
||||
filters: [
|
||||
['Chat Message', 'user', '==', frappe.session.user, true]
|
||||
// I need an or_filter here.
|
||||
// ['Chat Room', 'owner', '==', frappe.session.user, true],
|
||||
// ['Chat Room', frappe.session.user, 'in', 'users', true]
|
||||
]
|
||||
};
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
/* eslint semi: "never" */
|
||||
frappe.ui.form.on('Chat Profile', {
|
||||
refresh: function (form) {
|
||||
if ( form.doc.name !== frappe.session.user ) {
|
||||
form.disable_save()
|
||||
form.set_read_only(true)
|
||||
// There's one more that faris@frappe.io told me to add here. form.refresh_fields()?
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
{
|
||||
"autoname": "field:user",
|
||||
"beta": 1,
|
||||
"creation": "2017-11-13 18:26:57.943027",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"user",
|
||||
"status",
|
||||
"chat_background",
|
||||
"notifications",
|
||||
"message_preview",
|
||||
"notification_tones",
|
||||
"conversation_tones",
|
||||
"settings",
|
||||
"enable_chat"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Online",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Online\nAway\nBusy\nOffline"
|
||||
},
|
||||
{
|
||||
"fieldname": "chat_background",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Chat Background"
|
||||
},
|
||||
{
|
||||
"fieldname": "notifications",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Notifications"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "message_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "Message Preview"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "notification_tones",
|
||||
"fieldtype": "Check",
|
||||
"label": "Notification Tones"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "conversation_tones",
|
||||
"fieldtype": "Check",
|
||||
"label": "Conversation Tones"
|
||||
},
|
||||
{
|
||||
"fieldname": "settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Settings"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "enable_chat",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Chat"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"modified": "2019-11-07 13:21:36.414961",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Chat",
|
||||
"name": "Chat Profile",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
from frappe import _
|
||||
import frappe
|
||||
|
||||
# imports - frappe module imports
|
||||
from frappe.core.doctype.version.version import get_diff
|
||||
from frappe.chat.doctype.chat_room import chat_room
|
||||
from frappe.chat.util import (
|
||||
safe_json_loads,
|
||||
filter_dict,
|
||||
dictify
|
||||
)
|
||||
|
||||
session = frappe.session
|
||||
|
||||
class ChatProfile(Document):
|
||||
def on_update(self):
|
||||
if not self.is_new():
|
||||
b, a = self.get_doc_before_save(), self
|
||||
diff = dictify(get_diff(a, b))
|
||||
if diff:
|
||||
user = session.user
|
||||
|
||||
fields = [changed[0] for changed in diff.changed]
|
||||
|
||||
if 'status' in fields:
|
||||
rooms = chat_room.get(user, filters = ['Chat Room', 'type', '=', 'Direct'])
|
||||
update = dict(user = user, data = dict(status = self.status))
|
||||
|
||||
for room in rooms:
|
||||
frappe.publish_realtime('frappe.chat.profile:update', update, room = room.name, after_commit = True)
|
||||
|
||||
if 'enable_chat' in fields:
|
||||
update = dict(user = user, data = dict(enable_chat = bool(self.enable_chat)))
|
||||
frappe.publish_realtime('frappe.chat.profile:update', update, user = user, after_commit = True)
|
||||
|
||||
def authenticate(user):
|
||||
if user != session.user:
|
||||
frappe.throw(_("Sorry, you're not authorized."))
|
||||
|
||||
@frappe.whitelist()
|
||||
def get(user, fields = None):
|
||||
duser = frappe.get_doc('User', user)
|
||||
|
||||
if frappe.db.exists('Chat Profile', user):
|
||||
dprof = frappe.get_doc('Chat Profile', user)
|
||||
|
||||
# If you're adding something here, make sure the client recieves it.
|
||||
profile = dict(
|
||||
# User
|
||||
name = duser.name,
|
||||
email = duser.email,
|
||||
first_name = duser.first_name,
|
||||
last_name = duser.last_name,
|
||||
username = duser.username,
|
||||
avatar = duser.user_image,
|
||||
bio = duser.bio,
|
||||
# Chat Profile
|
||||
status = dprof.status,
|
||||
chat_background = dprof.chat_background,
|
||||
message_preview = bool(dprof.message_preview),
|
||||
notification_tones = bool(dprof.notification_tones),
|
||||
conversation_tones = bool(dprof.conversation_tones),
|
||||
enable_chat = bool(dprof.enable_chat)
|
||||
)
|
||||
profile = filter_dict(profile, fields)
|
||||
|
||||
return dictify(profile)
|
||||
|
||||
@frappe.whitelist()
|
||||
def create(user, exists_ok = False, fields = None):
|
||||
authenticate(user)
|
||||
|
||||
exists_ok, fields = safe_json_loads(exists_ok, fields)
|
||||
|
||||
try:
|
||||
dprof = frappe.new_doc('Chat Profile')
|
||||
dprof.user = user
|
||||
dprof.save(ignore_permissions = True)
|
||||
except frappe.DuplicateEntryError:
|
||||
frappe.clear_messages()
|
||||
if not exists_ok:
|
||||
frappe.throw(_('Chat Profile for User {0} exists.').format(user))
|
||||
|
||||
profile = get(user, fields = fields)
|
||||
|
||||
return profile
|
||||
|
||||
@frappe.whitelist()
|
||||
def update(user, data):
|
||||
authenticate(user)
|
||||
|
||||
data = safe_json_loads(data)
|
||||
|
||||
dprof = frappe.get_doc('Chat Profile', user)
|
||||
dprof.update(data)
|
||||
dprof.save(ignore_permissions = True)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
frappe.listview_settings['Chat Profile'] =
|
||||
{
|
||||
get_indicator: function (doc)
|
||||
{
|
||||
const status = frappe.utils.squash(frappe.chat.profile.STATUSES.filter(
|
||||
s => s.name === doc.status
|
||||
));
|
||||
|
||||
return [__(status.name), status.color, `status,=,${status.name}`]
|
||||
}
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
// Copyright (c) 2017, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Chat Room', {
|
||||
refresh: function (form) {
|
||||
|
||||
}
|
||||
});
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
{
|
||||
"autoname": "CR.#####",
|
||||
"beta": 1,
|
||||
"creation": "2017-11-08 15:27:21.156667",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"type",
|
||||
"room_name",
|
||||
"avatar",
|
||||
"last_message",
|
||||
"message_count",
|
||||
"owner",
|
||||
"user_list",
|
||||
"users"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "Direct",
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"options": "Direct\nGroup\nVisitor",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.type==\"Group\"",
|
||||
"fieldname": "room_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Name"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.type==\"Group\"",
|
||||
"fieldname": "avatar",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 1,
|
||||
"label": "Avatar"
|
||||
},
|
||||
{
|
||||
"fieldname": "last_message",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Last Message"
|
||||
},
|
||||
{
|
||||
"fieldname": "message_count",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 1,
|
||||
"label": "Message Count"
|
||||
},
|
||||
{
|
||||
"fieldname": "owner",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Owner",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "user_list",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Users"
|
||||
},
|
||||
{
|
||||
"fieldname": "users",
|
||||
"fieldtype": "Table",
|
||||
"label": "Users",
|
||||
"options": "Chat Room User"
|
||||
}
|
||||
],
|
||||
"image_field": "avatar",
|
||||
"modified": "2019-11-07 13:20:24.625329",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Chat",
|
||||
"name": "Chat Room",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "room_name",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "room_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
from frappe import _
|
||||
import frappe
|
||||
|
||||
# imports - frappe module imports
|
||||
from frappe.chat import authenticate
|
||||
from frappe.core.doctype.version.version import get_diff
|
||||
from frappe.chat.doctype.chat_message import chat_message
|
||||
from frappe.chat.util import (
|
||||
safe_json_loads,
|
||||
dictify,
|
||||
listify,
|
||||
squashify,
|
||||
get_if_empty
|
||||
)
|
||||
|
||||
session = frappe.session
|
||||
|
||||
|
||||
def is_direct(owner, other, bidirectional=False):
|
||||
def get_room(owner, other):
|
||||
room = frappe.get_all('Chat Room', filters=[
|
||||
['Chat Room', 'type', 'in', ('Direct', 'Visitor')],
|
||||
['Chat Room', 'owner', '=', owner],
|
||||
['Chat Room User', 'user', '=', other]
|
||||
], distinct=True)
|
||||
|
||||
return room
|
||||
|
||||
exists = len(get_room(owner, other)) == 1
|
||||
if bidirectional:
|
||||
exists = exists or len(get_room(other, owner)) == 1
|
||||
|
||||
return exists
|
||||
|
||||
|
||||
def get_chat_room_user_set(users, filter_=None):
|
||||
seen, uset = set(), list()
|
||||
|
||||
for u in users:
|
||||
if filter_(u) and u.user not in seen:
|
||||
uset.append(u)
|
||||
seen.add(u.user)
|
||||
|
||||
return uset
|
||||
|
||||
|
||||
class ChatRoom(Document):
|
||||
def validate(self):
|
||||
if self.is_new():
|
||||
users = get_chat_room_user_set(self.users, filter_=lambda u: u.user != session.user)
|
||||
self.update(dict(
|
||||
users=users
|
||||
))
|
||||
|
||||
if self.type == "Direct":
|
||||
if len(self.users) != 1:
|
||||
frappe.throw(_('{0} room must have atmost one user.').format(self.type))
|
||||
|
||||
other = squashify(self.users)
|
||||
|
||||
if self.is_new():
|
||||
if is_direct(self.owner, other.user, bidirectional=True):
|
||||
frappe.throw(_('Direct room with {0} already exists.').format(other.user))
|
||||
|
||||
if self.type == "Group" and not self.room_name:
|
||||
frappe.throw(_('Group name cannot be empty.'))
|
||||
|
||||
def on_update(self):
|
||||
if not self.is_new():
|
||||
before = self.get_doc_before_save()
|
||||
if not before: return
|
||||
|
||||
after = self
|
||||
diff = dictify(get_diff(before, after))
|
||||
if diff:
|
||||
update = {}
|
||||
for changed in diff.changed:
|
||||
field, old, new = changed
|
||||
|
||||
if field == 'last_message':
|
||||
new = chat_message.get(new)
|
||||
|
||||
update.update({field: new})
|
||||
|
||||
if diff.added or diff.removed:
|
||||
update.update(dict(users=[u.user for u in self.users]))
|
||||
|
||||
update = dict(room=self.name, data=update)
|
||||
|
||||
frappe.publish_realtime('frappe.chat.room:update', update, room=self.name,
|
||||
after_commit=True)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get(user=None, token=None, rooms=None, fields=None, filters=None):
|
||||
# There is this horrible bug out here.
|
||||
# Looks like if frappe.call sends optional arguments (not in right order),
|
||||
# the argument turns to an empty string.
|
||||
# I'm not even going to think searching for it.
|
||||
# Hence, the hack was get_if_empty (previous assign_if_none)
|
||||
# - Achilles Rasquinha achilles@frappe.io
|
||||
data = user or token
|
||||
authenticate(data)
|
||||
|
||||
rooms, fields, filters = safe_json_loads(rooms, fields, filters)
|
||||
|
||||
rooms = listify(get_if_empty(rooms, []))
|
||||
fields = listify(get_if_empty(fields, []))
|
||||
|
||||
const = [] # constraints
|
||||
if rooms:
|
||||
const.append(['Chat Room', 'name', 'in', rooms])
|
||||
if filters:
|
||||
if isinstance(filters[0], list):
|
||||
const = const + filters
|
||||
else:
|
||||
const.append(filters)
|
||||
|
||||
default = ['name', 'type', 'room_name', 'creation', 'owner', 'avatar']
|
||||
handle = ['users', 'last_message']
|
||||
|
||||
param = [f for f in fields if f not in handle]
|
||||
|
||||
rooms = frappe.get_all('Chat Room',
|
||||
or_filters=[
|
||||
['Chat Room', 'owner', '=', frappe.session.user],
|
||||
['Chat Room User', 'user', '=', frappe.session.user]
|
||||
],
|
||||
filters=const,
|
||||
fields=param + ['name'] if param else default,
|
||||
distinct=True
|
||||
)
|
||||
|
||||
if not fields or 'users' in fields:
|
||||
for i, r in enumerate(rooms):
|
||||
droom = frappe.get_doc('Chat Room', r.name)
|
||||
rooms[i]['users'] = []
|
||||
|
||||
for duser in droom.users:
|
||||
rooms[i]['users'].append(duser.user)
|
||||
|
||||
if not fields or 'last_message' in fields:
|
||||
for i, r in enumerate(rooms):
|
||||
droom = frappe.get_doc('Chat Room', r.name)
|
||||
if droom.last_message:
|
||||
rooms[i]['last_message'] = chat_message.get(droom.last_message)
|
||||
else:
|
||||
rooms[i]['last_message'] = None
|
||||
|
||||
rooms = squashify(dictify(rooms))
|
||||
|
||||
return rooms
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def create(kind, token, users=None, name=None):
|
||||
authenticate(token)
|
||||
|
||||
users = safe_json_loads(users)
|
||||
create = True
|
||||
|
||||
if kind == 'Visitor':
|
||||
room = squashify(frappe.db.sql("""
|
||||
SELECT name
|
||||
FROM `tabChat Room`
|
||||
WHERE owner=%s
|
||||
""", (frappe.session.user), as_dict=True))
|
||||
|
||||
if room:
|
||||
room = frappe.get_doc('Chat Room', room.name)
|
||||
create = False
|
||||
|
||||
if create:
|
||||
room = frappe.new_doc('Chat Room')
|
||||
room.type = kind
|
||||
room.owner = frappe.session.user
|
||||
room.room_name = name
|
||||
|
||||
dusers = []
|
||||
|
||||
if kind != 'Visitor':
|
||||
if users:
|
||||
users = listify(users)
|
||||
for user in users:
|
||||
duser = frappe.new_doc('Chat Room User')
|
||||
duser.user = user
|
||||
dusers.append(duser)
|
||||
|
||||
room.users = dusers
|
||||
else:
|
||||
dsettings = frappe.get_single('Website Settings')
|
||||
room.room_name = dsettings.chat_room_name
|
||||
|
||||
users = [user for user in room.users] if hasattr(room, 'users') else []
|
||||
|
||||
for user in dsettings.chat_operators:
|
||||
if user.user not in users:
|
||||
# appending user to room.users will remove the user from chat_operators
|
||||
# this is undesirable, create a new Chat Room User instead
|
||||
chat_room_user = {"doctype": "Chat Room User", "user": user.user}
|
||||
room.append('users', chat_room_user)
|
||||
|
||||
room.save(ignore_permissions=True)
|
||||
|
||||
room = get(token=token, rooms=room.name)
|
||||
if room:
|
||||
users = [room.owner] + [u for u in room.users]
|
||||
|
||||
for user in users:
|
||||
frappe.publish_realtime('frappe.chat.room:create', room, user=user, after_commit=True)
|
||||
|
||||
return room
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def history(room, user, fields=None, limit=10, start=None, end=None):
|
||||
if frappe.get_doc('Chat Room', room).type != 'Visitor':
|
||||
authenticate(user)
|
||||
|
||||
fields = safe_json_loads(fields)
|
||||
|
||||
mess = chat_message.history(room, limit=limit, start=start, end=end)
|
||||
mess = squashify(mess)
|
||||
|
||||
return dictify(mess)
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
frappe.listview_settings['Chat Room'] = {
|
||||
filters: [
|
||||
['Chat Room', 'owner', '=', frappe.session.user, true],
|
||||
['Chat Room User', 'user', '=', frappe.session.user, true]
|
||||
]
|
||||
};
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
{
|
||||
"beta": 1,
|
||||
"creation": "2017-11-08 15:24:21.029314",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"user",
|
||||
"is_admin"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_admin",
|
||||
"fieldtype": "Check",
|
||||
"label": "Admin"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"istable": 1,
|
||||
"modified": "2019-11-07 13:21:05.297337",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Chat",
|
||||
"name": "Chat Room User",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
import frappe
|
||||
|
||||
session = frappe.session
|
||||
|
||||
class ChatRoomUser(Document):
|
||||
pass
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
// Copyright (c) 2018, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Chat Token', {
|
||||
refresh: function(frm) {
|
||||
|
||||
}
|
||||
});
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
{
|
||||
"autoname": "field:token",
|
||||
"beta": 1,
|
||||
"creation": "2018-03-26 18:20:13.825652",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"token",
|
||||
"ip_address",
|
||||
"country"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "token",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Token",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "ip_address",
|
||||
"fieldtype": "Data",
|
||||
"label": "IP Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Data",
|
||||
"label": "Country"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"modified": "2019-11-07 13:21:24.514558",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Chat",
|
||||
"name": "Chat Token",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2018, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class ChatToken(Document):
|
||||
pass
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# imports - module imports
|
||||
from frappe.chat.util.util import (
|
||||
get_user_doc,
|
||||
squashify,
|
||||
safe_json_loads,
|
||||
filter_dict,
|
||||
get_if_empty,
|
||||
listify,
|
||||
dictify,
|
||||
check_url,
|
||||
create_test_user,
|
||||
get_emojis
|
||||
)
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
# imports - standard imports
|
||||
import unittest
|
||||
|
||||
# imports - module imports
|
||||
from frappe.chat.util import (
|
||||
get_user_doc,
|
||||
safe_json_loads
|
||||
)
|
||||
import frappe
|
||||
|
||||
class TestChatUtil(unittest.TestCase):
|
||||
def test_safe_json_loads(self):
|
||||
number = safe_json_loads("1")
|
||||
self.assertEqual(type(number), int)
|
||||
|
||||
number = safe_json_loads("1.0")
|
||||
self.assertEqual(type(number), float)
|
||||
|
||||
string = safe_json_loads("foobar")
|
||||
self.assertEqual(type(string), str)
|
||||
|
||||
array = safe_json_loads('[{ "foo": "bar" }]')
|
||||
self.assertEqual(type(array), list)
|
||||
|
||||
objekt = safe_json_loads('{ "foo": "bar" }')
|
||||
self.assertEqual(type(objekt), dict)
|
||||
|
||||
true, null = safe_json_loads("true", "null")
|
||||
self.assertEqual(true, True)
|
||||
self.assertEqual(null, None)
|
||||
|
||||
def test_get_user_doc(self):
|
||||
# Needs more test cases.
|
||||
user = get_user_doc()
|
||||
self.assertEqual(user.name, frappe.session.user)
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
# imports - standard imports
|
||||
import json
|
||||
from collections.abc import MutableMapping, MutableSequence, Sequence
|
||||
|
||||
# imports - third-party imports
|
||||
import requests
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# imports - module imports
|
||||
import frappe
|
||||
from frappe.exceptions import DuplicateEntryError
|
||||
from frappe.model.document import Document
|
||||
|
||||
session = frappe.session
|
||||
|
||||
|
||||
def get_user_doc(user = None):
|
||||
if isinstance(user, Document):
|
||||
return user
|
||||
|
||||
user = user or session.user
|
||||
user = frappe.get_doc('User', user)
|
||||
|
||||
return user
|
||||
|
||||
def squashify(what):
|
||||
if isinstance(what, Sequence) and len(what) == 1:
|
||||
return what[0]
|
||||
|
||||
return what
|
||||
|
||||
def safe_json_loads(*args):
|
||||
results = []
|
||||
|
||||
for arg in args:
|
||||
try:
|
||||
arg = json.loads(arg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
results.append(arg)
|
||||
|
||||
return squashify(results)
|
||||
|
||||
def filter_dict(what, keys, ignore = False):
|
||||
copy = dict()
|
||||
|
||||
if keys:
|
||||
for k in keys:
|
||||
if k not in what and not ignore:
|
||||
raise KeyError('{key} not in dict.'.format(key = k))
|
||||
else:
|
||||
copy.update({
|
||||
k: what[k]
|
||||
})
|
||||
else:
|
||||
copy = what.copy()
|
||||
|
||||
return copy
|
||||
|
||||
def get_if_empty(a, b):
|
||||
if not a:
|
||||
a = b
|
||||
return a
|
||||
|
||||
def listify(arg):
|
||||
if not isinstance(arg, list):
|
||||
arg = [arg]
|
||||
return arg
|
||||
|
||||
def dictify(arg):
|
||||
if isinstance(arg, MutableSequence):
|
||||
for i, a in enumerate(arg):
|
||||
arg[i] = dictify(a)
|
||||
elif isinstance(arg, MutableMapping):
|
||||
arg = frappe._dict(arg)
|
||||
|
||||
return arg
|
||||
|
||||
def check_url(what, raise_err = False):
|
||||
if not urlparse(what).scheme:
|
||||
if raise_err:
|
||||
raise ValueError('{what} not a valid URL.')
|
||||
else:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def create_test_user(module):
|
||||
try:
|
||||
test_user = frappe.new_doc('User')
|
||||
test_user.first_name = '{module}'.format(module = module)
|
||||
test_user.email = 'testuser.{module}@example.com'.format(module = module)
|
||||
test_user.save()
|
||||
except DuplicateEntryError:
|
||||
frappe.log('Test User Chat Profile exists.')
|
||||
|
||||
def get_emojis():
|
||||
redis = frappe.cache()
|
||||
emojis = redis.hget('frappe_emojis', 'emojis')
|
||||
|
||||
if not emojis:
|
||||
resp = requests.get('http://git.io/frappe-emoji')
|
||||
if resp.ok:
|
||||
emojis = resp.json()
|
||||
redis.hset('frappe_emojis', 'emojis', emojis)
|
||||
|
||||
return dictify(emojis)
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
|
||||
import frappe
|
||||
from frappe.chat.util import filter_dict, safe_json_loads
|
||||
|
||||
from frappe.sessions import get_geo_ip_country
|
||||
|
||||
@frappe.whitelist(allow_guest = True)
|
||||
def settings(fields = None):
|
||||
fields = safe_json_loads(fields)
|
||||
|
||||
dsettings = frappe.get_single('Website Settings')
|
||||
response = dict(
|
||||
socketio = dict(
|
||||
port = frappe.conf.socketio_port
|
||||
),
|
||||
enable = bool(dsettings.chat_enable),
|
||||
enable_from = dsettings.chat_enable_from,
|
||||
enable_to = dsettings.chat_enable_to,
|
||||
room_name = dsettings.chat_room_name,
|
||||
welcome_message = dsettings.chat_welcome_message,
|
||||
operators = [
|
||||
duser.user for duser in dsettings.chat_operators
|
||||
]
|
||||
)
|
||||
|
||||
if fields:
|
||||
response = filter_dict(response, fields)
|
||||
|
||||
return response
|
||||
|
||||
@frappe.whitelist(allow_guest = True)
|
||||
def token():
|
||||
dtoken = frappe.new_doc('Chat Token')
|
||||
|
||||
dtoken.token = frappe.generate_hash()
|
||||
dtoken.ip_address = frappe.local.request_ip
|
||||
country = get_geo_ip_country(dtoken.ip_address)
|
||||
if country:
|
||||
dtoken.country = country['iso_code']
|
||||
dtoken.save(ignore_permissions = True)
|
||||
|
||||
return dtoken.token
|
||||
|
|
@ -55,8 +55,11 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
|
|||
@click.option('--with-public-files', help='Restores the public files of the site, given path to its tar file')
|
||||
@click.option('--with-private-files', help='Restores the private files of the site, given path to its tar file')
|
||||
@click.option('--force', is_flag=True, default=False, help='Ignore the validations and downgrade warnings. This action is not recommended')
|
||||
@click.option('--encryption-key', help='Backup encryption key')
|
||||
@pass_context
|
||||
def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None):
|
||||
def restore(context, sql_file_path, encryption_key=None, mariadb_root_username=None, mariadb_root_password=None,
|
||||
db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None,
|
||||
with_private_files=None):
|
||||
"Restore site database from an sql file"
|
||||
from frappe.installer import (
|
||||
_new_site,
|
||||
|
|
@ -66,26 +69,74 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
|
|||
is_partial,
|
||||
validate_database_sql
|
||||
)
|
||||
from frappe.utils.backups import Backup
|
||||
if not os.path.exists(sql_file_path):
|
||||
print("Invalid path", sql_file_path)
|
||||
sys.exit(1)
|
||||
|
||||
_backup = Backup(sql_file_path)
|
||||
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
|
||||
force = context.force or force
|
||||
decompressed_file_name = extract_sql_from_archive(sql_file_path)
|
||||
|
||||
# check if partial backup
|
||||
if is_partial(decompressed_file_name):
|
||||
click.secho(
|
||||
"Partial Backup file detected. You cannot use a partial file to restore a Frappe Site.",
|
||||
fg="red"
|
||||
)
|
||||
click.secho(
|
||||
"Use `bench partial-restore` to restore a partial backup to an existing site.",
|
||||
fg="yellow"
|
||||
)
|
||||
sys.exit(1)
|
||||
try:
|
||||
decompressed_file_name = extract_sql_from_archive(sql_file_path)
|
||||
if is_partial(decompressed_file_name):
|
||||
click.secho(
|
||||
"Partial Backup file detected. You cannot use a partial file to restore a Frappe site.",
|
||||
fg="red"
|
||||
)
|
||||
click.secho(
|
||||
"Use `bench partial-restore` to restore a partial backup to an existing site.",
|
||||
fg="yellow"
|
||||
)
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
||||
except UnicodeDecodeError:
|
||||
_backup.decryption_rollback()
|
||||
if encryption_key:
|
||||
click.secho(
|
||||
"Encrypted backup file detected. Decrypting using provided key.",
|
||||
fg="yellow"
|
||||
)
|
||||
_backup.backup_decryption(encryption_key)
|
||||
|
||||
else:
|
||||
click.secho(
|
||||
"Encrypted backup file detected. Decrypting using site config.",
|
||||
fg="yellow"
|
||||
)
|
||||
encryption_key = frappe.get_site_config().encryption_key
|
||||
_backup.backup_decryption(encryption_key)
|
||||
|
||||
# Rollback on unsuccessful decryrption
|
||||
if not os.path.exists(sql_file_path):
|
||||
click.secho(
|
||||
"Decryption failed. Please provide a valid key and try again.",
|
||||
fg="red"
|
||||
)
|
||||
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
||||
decompressed_file_name = extract_sql_from_archive(sql_file_path)
|
||||
|
||||
if is_partial(decompressed_file_name):
|
||||
click.secho(
|
||||
"Partial Backup file detected. You cannot use a partial file to restore a Frappe site.",
|
||||
fg="red"
|
||||
)
|
||||
click.secho(
|
||||
"Use `bench partial-restore` to restore a partial backup to an existing site.",
|
||||
fg="yellow"
|
||||
)
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
# check if valid SQL file
|
||||
validate_database_sql(decompressed_file_name, _raise=not force)
|
||||
|
||||
# dont allow downgrading to older versions of frappe without force
|
||||
|
|
@ -96,23 +147,51 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
|
|||
)
|
||||
click.confirm(warn_message, abort=True)
|
||||
|
||||
_new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username,
|
||||
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
|
||||
verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name,
|
||||
force=True, db_type=frappe.conf.db_type)
|
||||
|
||||
# Extract public and/or private files to the restored site, if user has given the path
|
||||
if with_public_files:
|
||||
public = extract_files(site, with_public_files)
|
||||
os.remove(public)
|
||||
|
||||
if with_private_files:
|
||||
private = extract_files(site, with_private_files)
|
||||
os.remove(private)
|
||||
try:
|
||||
_new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username,
|
||||
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
|
||||
verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name,
|
||||
force=True, db_type=frappe.conf.db_type)
|
||||
|
||||
except Exception as err:
|
||||
print(err.args[1])
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
||||
# Removing temporarily created file
|
||||
if decompressed_file_name != sql_file_path:
|
||||
os.remove(decompressed_file_name)
|
||||
_backup.decryption_rollback()
|
||||
|
||||
# Extract public and/or private files to the restored site, if user has given the path
|
||||
if with_public_files:
|
||||
# Decrypt data if there is a Key
|
||||
if encryption_key:
|
||||
_backup = Backup(with_public_files)
|
||||
_backup.backup_decryption(encryption_key)
|
||||
if not os.path.exists(with_public_files):
|
||||
_backup.decryption_rollback()
|
||||
public = extract_files(site, with_public_files)
|
||||
|
||||
# Removing temporarily created file
|
||||
os.remove(public)
|
||||
_backup.decryption_rollback()
|
||||
|
||||
|
||||
if with_private_files:
|
||||
# Decrypt data if there is a Key
|
||||
if encryption_key:
|
||||
_backup = Backup(with_private_files)
|
||||
_backup.backup_decryption(encryption_key)
|
||||
if not os.path.exists(with_private_files):
|
||||
_backup.decryption_rollback()
|
||||
private = extract_files(site, with_private_files)
|
||||
|
||||
# Removing temporarily created file
|
||||
os.remove(private)
|
||||
_backup.decryption_rollback()
|
||||
|
||||
success_message = "Site {0} has been restored{1}".format(
|
||||
site,
|
||||
|
|
@ -120,19 +199,92 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
|
|||
)
|
||||
click.secho(success_message, fg="green")
|
||||
|
||||
|
||||
@click.command('partial-restore')
|
||||
@click.argument('sql-file-path')
|
||||
@click.option("--verbose", "-v", is_flag=True)
|
||||
@click.option('--encryption-key', help='Backup encryption key')
|
||||
@pass_context
|
||||
def partial_restore(context, sql_file_path, verbose):
|
||||
from frappe.installer import partial_restore
|
||||
verbose = context.verbose or verbose
|
||||
def partial_restore(context, sql_file_path, verbose, encryption_key=None):
|
||||
from frappe.installer import partial_restore, extract_sql_from_archive
|
||||
from frappe.utils.backups import Backup
|
||||
|
||||
if not os.path.exists(sql_file_path):
|
||||
print("Invalid path", sql_file_path)
|
||||
sys.exit(1)
|
||||
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
|
||||
_backup = Backup(sql_file_path)
|
||||
|
||||
verbose = context.verbose or verbose
|
||||
|
||||
frappe.connect(site=site)
|
||||
try:
|
||||
decompressed_file_name = extract_sql_from_archive(sql_file_path)
|
||||
|
||||
with open(decompressed_file_name) as f:
|
||||
header = " ".join(f.readline() for _ in range(5))
|
||||
|
||||
#Check for full backup file
|
||||
if "Partial Backup" not in header:
|
||||
click.secho(
|
||||
"Full backup file detected.Use `bench restore` to restore a Frappe Site.",
|
||||
fg="red"
|
||||
)
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
except UnicodeDecodeError:
|
||||
_backup.decryption_rollback()
|
||||
if encryption_key:
|
||||
click.secho(
|
||||
"Encrypted backup file detected. Decrypting using provided key.",
|
||||
fg="yellow"
|
||||
)
|
||||
key = encryption_key
|
||||
|
||||
else:
|
||||
click.secho(
|
||||
"Encrypted backup file detected. Decrypting using site config.",
|
||||
fg="yellow"
|
||||
)
|
||||
key = frappe.get_site_config().encryption_key
|
||||
|
||||
_backup.backup_decryption(key)
|
||||
|
||||
# Rollback on unsuccessful decryrption
|
||||
if not os.path.exists(sql_file_path):
|
||||
click.secho(
|
||||
"Decryption failed. Please provide a valid key and try again.",
|
||||
fg="red"
|
||||
)
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
||||
decompressed_file_name = extract_sql_from_archive(sql_file_path)
|
||||
|
||||
with open(decompressed_file_name) as f:
|
||||
header = " ".join(f.readline() for _ in range(5))
|
||||
|
||||
#Check for Full backup file.
|
||||
if "Partial Backup" not in header:
|
||||
click.secho(
|
||||
"Full Backup file detected.Use `bench restore` to restore a Frappe Site.",
|
||||
fg="red"
|
||||
)
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
partial_restore(sql_file_path, verbose)
|
||||
|
||||
# Removing temporarily created file
|
||||
_backup.decryption_rollback()
|
||||
if os.path.exists(sql_file_path.rstrip(".gz")):
|
||||
os.remove(sql_file_path.rstrip(".gz"))
|
||||
|
||||
frappe.destroy()
|
||||
|
||||
|
||||
|
|
@ -418,6 +570,7 @@ def backup(context, with_files=False, backup_path=None, backup_path_db=None, bac
|
|||
backup_path_private_files=None, backup_path_conf=None, ignore_backup_conf=False, verbose=False,
|
||||
compress=False, include="", exclude=""):
|
||||
"Backup"
|
||||
|
||||
from frappe.utils.backups import scheduled_backup
|
||||
verbose = verbose or context.verbose
|
||||
exit_code = 0
|
||||
|
|
@ -441,14 +594,25 @@ def backup(context, with_files=False, backup_path=None, backup_path_db=None, bac
|
|||
force=True
|
||||
)
|
||||
except Exception:
|
||||
click.secho("Backup failed for Site {0}. Database or site_config.json may be corrupted".format(site), fg="red")
|
||||
click.secho(
|
||||
"Backup failed for Site {0}. Database or site_config.json may be corrupted".format(site),
|
||||
fg="red"
|
||||
)
|
||||
if verbose:
|
||||
print(frappe.get_traceback())
|
||||
exit_code = 1
|
||||
continue
|
||||
if frappe.get_system_settings("encrypt_backup") and frappe.get_site_config().encryption_key:
|
||||
click.secho(
|
||||
"Backup encryption is turned on. Please note the backup encryption key.",
|
||||
fg="yellow"
|
||||
)
|
||||
|
||||
odb.print_summary()
|
||||
click.secho("Backup for Site {0} has been successfully completed{1}".format(site, " with files" if with_files else ""), fg="green")
|
||||
click.secho(
|
||||
"Backup for Site {0} has been successfully completed{1}".format(site, " with files" if with_files else ""),
|
||||
fg="green"
|
||||
)
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
|
|
@ -456,6 +620,7 @@ def backup(context, with_files=False, backup_path=None, backup_path_db=None, bac
|
|||
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
@click.command('remove-from-installed-apps')
|
||||
@click.argument('app')
|
||||
@pass_context
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
"navigation_settings_section",
|
||||
"search_bar",
|
||||
"notifications",
|
||||
"chat",
|
||||
"list_settings_section",
|
||||
"list_sidebar",
|
||||
"bulk_actions",
|
||||
|
|
@ -85,12 +84,6 @@
|
|||
"fieldtype": "Check",
|
||||
"label": "Search Bar"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "chat",
|
||||
"fieldtype": "Check",
|
||||
"label": "Chat"
|
||||
},
|
||||
{
|
||||
"fieldname": "list_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
|
|
@ -155,10 +148,11 @@
|
|||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-27 10:35:37.638350",
|
||||
"modified": "2021-10-08 14:06:55.729364",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Role",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import frappe
|
|||
|
||||
from frappe.model.document import Document
|
||||
|
||||
desk_properties = ("search_bar", "notifications", "chat", "list_sidebar",
|
||||
desk_properties = ("search_bar", "notifications", "list_sidebar",
|
||||
"bulk_actions", "view_switcher", "form_sidebar", "timeline", "dashboard")
|
||||
|
||||
class Role(Document):
|
||||
|
|
@ -82,4 +82,4 @@ def role_query(doctype, txt, searchfield, start, page_len, filters):
|
|||
report_filters.extend(filters)
|
||||
|
||||
return frappe.get_all('Role', limit_start=start, limit_page_length=page_len,
|
||||
filters=report_filters, as_list=1)
|
||||
filters=report_filters, as_list=1)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
"currency_precision",
|
||||
"sec_backup_limit",
|
||||
"backup_limit",
|
||||
"encrypt_backup",
|
||||
"background_workers",
|
||||
"enable_scheduler",
|
||||
"dormant_days",
|
||||
|
|
@ -65,9 +66,7 @@
|
|||
"attach_view_link",
|
||||
"prepared_report_section",
|
||||
"enable_prepared_report_auto_deletion",
|
||||
"prepared_report_expiry_period",
|
||||
"chat",
|
||||
"enable_chat"
|
||||
"prepared_report_expiry_period"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -381,18 +380,6 @@
|
|||
"fieldtype": "Check",
|
||||
"label": "Hide footer in auto email reports"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "chat",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Chat"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "enable_chat",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Chat"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_21",
|
||||
"fieldtype": "Column Break"
|
||||
|
|
@ -469,12 +456,18 @@
|
|||
"fieldname": "strip_exif_metadata_from_uploaded_images",
|
||||
"fieldtype": "Check",
|
||||
"label": "Strip EXIF tags from uploaded images"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "encrypt_backup",
|
||||
"fieldtype": "Check",
|
||||
"label": "Encrypt Backups"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-30 11:47:47.330437",
|
||||
"modified": "2021-10-21 19:24:15.232430",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
@ -492,4 +485,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2018, Frappe Technologies and contributors
|
||||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import hashlib
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.utils import cint, now_datetime
|
||||
import hashlib
|
||||
|
||||
class TransactionLog(Document):
|
||||
def before_insert(self):
|
||||
|
|
@ -44,10 +45,14 @@ class TransactionLog(Document):
|
|||
|
||||
|
||||
def get_current_index():
|
||||
current = frappe.db.sql("""SELECT `current`
|
||||
FROM `tabSeries`
|
||||
WHERE `name` = 'TRANSACTLOG'
|
||||
FOR UPDATE""")
|
||||
series = DocType("Series")
|
||||
current = (
|
||||
frappe.qb.from_(series)
|
||||
.where(series.name == "TRANSACTLOG")
|
||||
.for_update()
|
||||
.select("current")
|
||||
).run()
|
||||
|
||||
if current and current[0][0] is not None:
|
||||
current = current[0][0]
|
||||
|
||||
|
|
|
|||
|
|
@ -263,6 +263,7 @@ frappe.ui.form.on('User', {
|
|||
callback: function(r) {
|
||||
if (r.message) {
|
||||
frappe.msgprint(__("Save API Secret: {0}", [r.message.api_secret]));
|
||||
frm.reload_doc();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -555,20 +555,22 @@
|
|||
"collapsible": 1,
|
||||
"fieldname": "api_access",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Api Access"
|
||||
"label": "API Access"
|
||||
},
|
||||
{
|
||||
"description": "API Key cannot be regenerated",
|
||||
"description": "API Key cannot be regenerated",
|
||||
"fieldname": "api_key",
|
||||
"fieldtype": "Data",
|
||||
"label": "API Key",
|
||||
"permlevel": 1,
|
||||
"read_only": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "generate_keys",
|
||||
"fieldtype": "Button",
|
||||
"label": "Generate Keys"
|
||||
"label": "Generate Keys",
|
||||
"permlevel": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_65",
|
||||
|
|
@ -578,6 +580,7 @@
|
|||
"fieldname": "api_secret",
|
||||
"fieldtype": "Password",
|
||||
"label": "API Secret",
|
||||
"permlevel": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -614,11 +617,6 @@
|
|||
"link_doctype": "Contact",
|
||||
"link_fieldname": "user"
|
||||
},
|
||||
{
|
||||
"group": "Profile",
|
||||
"link_doctype": "Chat Profile",
|
||||
"link_fieldname": "user"
|
||||
},
|
||||
{
|
||||
"group": "Profile",
|
||||
"link_doctype": "Blogger",
|
||||
|
|
@ -671,7 +669,7 @@
|
|||
}
|
||||
],
|
||||
"max_attachments": 5,
|
||||
"modified": "2021-10-18 16:56:05.578379",
|
||||
"modified": "2021-10-27 17:17:16.098457",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "User",
|
||||
|
|
@ -706,4 +704,4 @@
|
|||
"sort_order": "DESC",
|
||||
"title_field": "full_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -421,9 +421,6 @@ class User(Document):
|
|||
WHERE `%s` = %s""" %
|
||||
(tab, field, '%s', field, '%s'), (new_name, old_name))
|
||||
|
||||
if frappe.db.exists("Chat Profile", old_name):
|
||||
frappe.rename_doc("Chat Profile", old_name, new_name, force=True, show_alert=False)
|
||||
|
||||
if frappe.db.exists("Notification Settings", old_name):
|
||||
frappe.rename_doc("Notification Settings", old_name, new_name, force=True, show_alert=False)
|
||||
|
||||
|
|
@ -717,8 +714,10 @@ def ask_pass_update():
|
|||
# update the sys defaults as to awaiting users
|
||||
from frappe.utils import set_default
|
||||
|
||||
users = frappe.db.sql("""SELECT DISTINCT(parent) as user FROM `tabUser Email`
|
||||
WHERE awaiting_password = 1""", as_dict=True)
|
||||
doctype = DocType("User Email")
|
||||
users = frappe.qb.from_(doctype).where(doctype.awaiting_password == 1).select(
|
||||
doctype.parent.as_("user")
|
||||
).distinct().run(as_dict=True)
|
||||
|
||||
password_list = [ user.get("user") for user in users ]
|
||||
set_default("email_user_password", u','.join(password_list))
|
||||
|
|
@ -1055,4 +1054,4 @@ def get_enabled_users():
|
|||
enabled_users = frappe.get_all("User", filters={"enabled": "1"}, pluck="name")
|
||||
return enabled_users
|
||||
|
||||
return frappe.cache().get_value("enabled_users", _get_enabled_users)
|
||||
return frappe.cache().get_value("enabled_users", _get_enabled_users)
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class TestUserPermission(unittest.TestCase):
|
|||
def test_for_applicable_on_update_from_apply_to_all(self):
|
||||
''' Update User Permission from all to some applicable Doctypes'''
|
||||
user = create_user('test_bulk_creation_update@example.com')
|
||||
param = get_params(user,'User', user.name, applicable = ["Chat Room", "Chat Message"])
|
||||
param = get_params(user,'User', user.name, applicable = ["Comment", "Contact"])
|
||||
|
||||
# Initially create User Permission document with apply_to_all checked
|
||||
is_created = add_user_permissions(get_params(user, 'User', user.name))
|
||||
|
|
@ -84,8 +84,8 @@ class TestUserPermission(unittest.TestCase):
|
|||
frappe.db.commit()
|
||||
|
||||
removed_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user))
|
||||
is_created_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room"))
|
||||
is_created_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message"))
|
||||
is_created_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Comment"))
|
||||
is_created_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Contact"))
|
||||
|
||||
# Check that apply_to_all is removed
|
||||
self.assertIsNone(removed_apply_to_all)
|
||||
|
|
@ -101,14 +101,14 @@ class TestUserPermission(unittest.TestCase):
|
|||
param = get_params(user, 'User', user.name)
|
||||
|
||||
# create User permissions that with applicable
|
||||
is_created = add_user_permissions(get_params(user, 'User', user.name, applicable = ["Chat Room", "Chat Message"]))
|
||||
is_created = add_user_permissions(get_params(user, 'User', user.name, applicable = ["Comment", "Contact"]))
|
||||
|
||||
self.assertEqual(is_created, 1)
|
||||
|
||||
is_created = add_user_permissions(param)
|
||||
is_created_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user))
|
||||
removed_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room"))
|
||||
removed_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message"))
|
||||
removed_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Comment"))
|
||||
removed_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Contact"))
|
||||
|
||||
# To check that a User permission with apply_to_all exists
|
||||
self.assertIsNotNone(is_created_apply_to_all)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
|
|
@ -12,13 +12,17 @@ def execute(filters=None):
|
|||
return columns, data
|
||||
|
||||
def get_data(filters=None):
|
||||
|
||||
logs = frappe.db.sql("SELECT * FROM `tabTransaction Log` order by creation desc ", as_dict=1)
|
||||
result = []
|
||||
logs = frappe.get_all("Transaction Log", fields=["*"], order_by="creation desc")
|
||||
|
||||
for l in logs:
|
||||
row_index = int(l.row_index)
|
||||
if row_index > 1:
|
||||
previous_hash = frappe.db.sql("SELECT chaining_hash FROM `tabTransaction Log` WHERE row_index = {0}".format(row_index - 1))
|
||||
previous_hash = frappe.get_all(
|
||||
"Transaction Log",
|
||||
fields=["chaining_hash"],
|
||||
filters={"row_index": row_index - 1},
|
||||
)
|
||||
if not previous_hash:
|
||||
integrity = False
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -43,20 +43,28 @@ class PropertySetter(Document):
|
|||
|
||||
def get_setup_data(self):
|
||||
return {
|
||||
'doctypes': [d[0] for d in frappe.db.sql("select name from tabDocType")],
|
||||
'doctypes': frappe.get_all("DocType", pluck="name"),
|
||||
'dt_properties': self.get_property_list('DocType'),
|
||||
'df_properties': self.get_property_list('DocField')
|
||||
}
|
||||
|
||||
def get_field_ids(self):
|
||||
return frappe.db.sql("select name, fieldtype, label, fieldname from tabDocField where parent=%s", self.doc_type, as_dict = 1)
|
||||
return frappe.db.get_values(
|
||||
"DocField",
|
||||
filters={"parent": self.doc_type},
|
||||
fieldname=["name", "fieldtype", "label", "fieldname"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
def get_defaults(self):
|
||||
if not self.field_name:
|
||||
return frappe.db.sql("select * from `tabDocType` where name=%s", self.doc_type, as_dict = 1)[0]
|
||||
return frappe.get_all("DocType", filters={"name": self.doc_type}, fields="*")[0]
|
||||
else:
|
||||
return frappe.db.sql("select * from `tabDocField` where fieldname=%s and parent=%s",
|
||||
(self.field_name, self.doc_type), as_dict = 1)[0]
|
||||
return frappe.db.get_values(
|
||||
"DocField",
|
||||
filters={"fieldname": self.field_name, "parent": self.doc_type},
|
||||
fieldname="*",
|
||||
)[0]
|
||||
|
||||
def on_update(self):
|
||||
if frappe.flags.in_patch:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from frappe.query_builder.functions import Count
|
|||
from frappe.query_builder.functions import Min, Max, Avg, Sum
|
||||
from frappe.query_builder.utils import Column
|
||||
from .query import Query
|
||||
from pypika.terms import PseudoColumn
|
||||
from pypika.terms import Criterion, PseudoColumn
|
||||
|
||||
|
||||
class Database(object):
|
||||
|
|
@ -543,18 +543,22 @@ class Database(object):
|
|||
update=None, for_update=False, run=True):
|
||||
field_objects = []
|
||||
|
||||
for field in fields:
|
||||
if "(" in field or " as " in field:
|
||||
field_objects.append(PseudoColumn(field))
|
||||
else:
|
||||
field_objects.append(field)
|
||||
if not isinstance(fields, Criterion):
|
||||
for field in fields:
|
||||
if "(" in field or " as " in field:
|
||||
field_objects.append(PseudoColumn(field))
|
||||
else:
|
||||
field_objects.append(field)
|
||||
|
||||
criterion = self.query.build_conditions(
|
||||
table=doctype, filters=filters, orderby=order_by, for_update=for_update
|
||||
)
|
||||
|
||||
if isinstance(fields, (list, tuple)):
|
||||
query = criterion.select(*field_objects)
|
||||
|
||||
elif isinstance(fields, Criterion):
|
||||
query = criterion.select(fields)
|
||||
|
||||
else:
|
||||
if fields=="*":
|
||||
query = criterion.select(fields)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import unique
|
||||
from frappe.query_builder import DocType
|
||||
|
||||
class Tag(Document):
|
||||
pass
|
||||
|
|
@ -11,7 +12,8 @@ class Tag(Document):
|
|||
def check_user_tags(dt):
|
||||
"if the user does not have a tags column, then it creates one"
|
||||
try:
|
||||
frappe.db.sql("select `_user_tags` from `tab%s` limit 1" % dt)
|
||||
doctype = DocType(dt)
|
||||
frappe.qb.from_(doctype).select(doctype._user_tags).limit(1).run()
|
||||
except Exception as e:
|
||||
if frappe.db.is_column_missing(e):
|
||||
DocTags(dt).setup()
|
||||
|
|
@ -42,10 +44,12 @@ def remove_tag(tag, dt, dn):
|
|||
@frappe.whitelist()
|
||||
def get_tagged_docs(doctype, tag):
|
||||
frappe.has_permission(doctype, throw=True)
|
||||
|
||||
return frappe.db.sql("""SELECT name
|
||||
FROM `tab{0}`
|
||||
WHERE _user_tags LIKE '%{1}%'""".format(doctype, tag))
|
||||
doctype = DocType(doctype)
|
||||
return (
|
||||
frappe.qb.from_(doctype)
|
||||
.where(doctype._user_tags.like(tag))
|
||||
.select(doctype.name)
|
||||
).run()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_tags(doctype, txt):
|
||||
|
|
|
|||
|
|
@ -1,20 +1,27 @@
|
|||
<!-- jinja -->
|
||||
<div class="row download-backups">
|
||||
{% for f in files %}
|
||||
<div class="col-lg-3 col-md-4 col-12">
|
||||
<a href="{{ f[0] }}" target="_blank" rel="noopener noreferrer" class="frappe-card download-backup-card">
|
||||
<div>
|
||||
{{ f[1] }}
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<svg class="icon-sm" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke="var(--icon-stroke)">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
{{ f[2] }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for f in files %}
|
||||
<div class="col-lg-3 col-md-4 col-12">
|
||||
<a href="{{ f[0] }}" target="_blank" rel="noopener noreferrer" class="frappe-card download-backup-card">
|
||||
<div>
|
||||
{{ f[1] }}
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="background-size: 20px 30px;" class="icon-sm" width="30"
|
||||
height="18" fill="none" viewBox="0 0 24 24" stroke="var(--icon-stroke)">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
{{ f[3] }}
|
||||
{% if f[2] %}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-lock" width="16" height="16" fill="currentColor"
|
||||
viewBox="0 0 48 48">
|
||||
<path
|
||||
d="M36 16h-2v-4c0-5.52-4.48-10-10-10s-10 4.48-10 10v4h-2c-2.21 0-4 1.79-4 4v20c0 2.21 1.79 4 4 4h24c2.21 0 4-1.79 4-4v-20c0-2.21-1.79-4-4-4zm-12-10.2c3.42 0 6.2 2.78 6.2 6.2v4h-12.2v-4h-.2c0-3.42 2.78-6.2 6.2-6.2zm12 34.2h-24v-20h24v20zm-12-6c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4z" />
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
frappe.pages['backups'].on_page_load = function(wrapper) {
|
||||
frappe.pages['backups'].on_page_load = function (wrapper) {
|
||||
var page = frappe.ui.make_app_page({
|
||||
parent: wrapper,
|
||||
title: __('Download Backups'),
|
||||
|
|
@ -11,12 +11,35 @@ frappe.pages['backups'].on_page_load = function(wrapper) {
|
|||
|
||||
page.add_inner_button(__("Download Files Backup"), function () {
|
||||
frappe.call({
|
||||
method:"frappe.desk.page.backups.backups.schedule_files_backup",
|
||||
args: {"user_email": frappe.session.user_email}
|
||||
method: "frappe.desk.page.backups.backups.schedule_files_backup",
|
||||
args: { "user_email": frappe.session.user_email }
|
||||
});
|
||||
});
|
||||
|
||||
page.add_inner_button(__("Get Backup Encryption Key"), function () {
|
||||
if (frappe.user.has_role("System Manager")) {
|
||||
frappe.verify_password(function () {
|
||||
frappe.call({
|
||||
method: "frappe.utils.backups.get_backup_encryption_key",
|
||||
callback: function (r) {
|
||||
frappe.msgprint({
|
||||
title: __('Backup Encryption Key'),
|
||||
message: __(r.message),
|
||||
indicator: 'blue'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
message: __('System Manager privileges required.'),
|
||||
indicator: 'red'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
frappe.breadcrumbs.add("Setup");
|
||||
|
||||
$(frappe.render_template("backups")).appendTo(page.body.addClass("no-border"));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ def get_context(context):
|
|||
dt = os.path.getmtime(path)
|
||||
return convert_utc_to_user_timezone(datetime.datetime.utcfromtimestamp(dt)).strftime('%a %b %d %H:%M %Y')
|
||||
|
||||
def get_encrytion_status(path):
|
||||
if "-enc" in path:
|
||||
return True
|
||||
|
||||
def get_size(path):
|
||||
size = os.path.getsize(path)
|
||||
if size > 1048576:
|
||||
|
|
@ -26,8 +30,9 @@ def get_context(context):
|
|||
cleanup_old_backups(path, files, backup_limit)
|
||||
|
||||
files = [('/backups/' + _file,
|
||||
get_time(os.path.join(path, _file)),
|
||||
get_size(os.path.join(path, _file))) for _file in files if _file.endswith('sql.gz')]
|
||||
get_time(os.path.join(path, _file)),
|
||||
get_encrytion_status(os.path.join(path, _file)),
|
||||
get_size(os.path.join(path, _file))) for _file in files if _file.endswith('sql.gz')]
|
||||
files.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
return {"files": files[:backup_limit]}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ def get_report_result(report, filters):
|
|||
|
||||
return res
|
||||
|
||||
@frappe.read_only()
|
||||
def generate_report_result(report, filters=None, user=None, custom_columns=None):
|
||||
user = user or frappe.session.user
|
||||
filters = filters or []
|
||||
|
|
@ -405,7 +406,7 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visi
|
|||
for column in data.columns:
|
||||
if column.get("hidden"):
|
||||
continue
|
||||
result[0].append(column["label"])
|
||||
result[0].append(column.get("label"))
|
||||
column_width = cint(column.get('width', 0))
|
||||
# to convert into scale accepted by openpyxl
|
||||
column_width /= 10
|
||||
|
|
|
|||
|
|
@ -242,13 +242,16 @@ def make_links(columns, data):
|
|||
for row in data:
|
||||
doc_name = row.get('name')
|
||||
for col in columns:
|
||||
if col.fieldtype == "Link" and col.options != "Currency":
|
||||
if col.options and row.get(col.fieldname):
|
||||
if not row.get(col.fieldname):
|
||||
continue
|
||||
|
||||
if col.fieldtype == "Link":
|
||||
if col.options and col.options != "Currency":
|
||||
row[col.fieldname] = get_link_to_form(col.options, row[col.fieldname])
|
||||
elif col.fieldtype == "Dynamic Link":
|
||||
if col.options and row.get(col.fieldname) and row.get(col.options):
|
||||
if col.options and row.get(col.options):
|
||||
row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname])
|
||||
elif col.fieldtype == "Currency" and row.get(col.fieldname):
|
||||
elif col.fieldtype == "Currency":
|
||||
doc = frappe.get_doc(col.parent, doc_name) if doc_name and col.parent else None
|
||||
# Pass the Document to get the currency based on docfield option
|
||||
row[col.fieldname] = frappe.format_value(row[col.fieldname], col, doc=doc)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ def handle():
|
|||
cmd = frappe.local.form_dict.cmd
|
||||
data = None
|
||||
|
||||
if cmd!='login':
|
||||
if cmd != 'login':
|
||||
data = execute_cmd(cmd)
|
||||
|
||||
# data can be an empty string or list which are valid responses
|
||||
|
|
|
|||
|
|
@ -76,8 +76,6 @@ before_tests = "frappe.utils.install.before_tests"
|
|||
|
||||
email_append_to = ["Event", "ToDo", "Communication"]
|
||||
|
||||
get_rooms = 'frappe.chat.doctype.chat_room.chat_room.get_rooms'
|
||||
|
||||
calendars = ["Event"]
|
||||
|
||||
leaderboards = "frappe.desk.leaderboard.get_leaderboards"
|
||||
|
|
@ -281,11 +279,6 @@ sounds = [
|
|||
{"name": "error", "src": "/assets/frappe/sounds/error.mp3", "volume": 0.1},
|
||||
{"name": "alert", "src": "/assets/frappe/sounds/alert.mp3", "volume": 0.2},
|
||||
# {"name": "chime", "src": "/assets/frappe/sounds/chime.mp3"},
|
||||
|
||||
# frappe.chat sounds
|
||||
{ "name": "chat-message", "src": "/assets/frappe/sounds/chat-message.mp3", "volume": 0.1 },
|
||||
{ "name": "chat-notification", "src": "/assets/frappe/sounds/chat-notification.mp3", "volume": 0.1 }
|
||||
# frappe.chat sounds
|
||||
]
|
||||
|
||||
bot_parsers = [
|
||||
|
|
|
|||
|
|
@ -80,7 +80,9 @@ class SocialLoginKey(Document):
|
|||
"redirect_url":"/api/method/frappe.www.login.login_via_github",
|
||||
"api_endpoint":"user",
|
||||
"api_endpoint_args":None,
|
||||
"auth_url_data":None
|
||||
"auth_url_data": json.dumps({
|
||||
"scope": "user:email"
|
||||
})
|
||||
}
|
||||
|
||||
providers["Google"] = {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@
|
|||
import frappe
|
||||
from frappe.integrations.doctype.social_login_key.social_login_key import BaseUrlNotSetError
|
||||
import unittest
|
||||
from frappe.utils.oauth import login_via_oauth2
|
||||
from unittest.mock import patch, MagicMock
|
||||
from rauth import OAuth2Service
|
||||
from frappe.auth import LoginManager, CookieManager
|
||||
from frappe.utils import set_request
|
||||
|
||||
|
||||
class TestSocialLoginKey(unittest.TestCase):
|
||||
def test_adding_frappe_social_login_provider(self):
|
||||
|
|
@ -14,6 +20,41 @@ class TestSocialLoginKey(unittest.TestCase):
|
|||
social_login_key.get_social_login_provider(provider_name, initialize=True)
|
||||
self.assertRaises(BaseUrlNotSetError, social_login_key.insert)
|
||||
|
||||
def test_github_login_with_private_email(self):
|
||||
github_social_login_setup()
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_session.get.side_effect = github_response_for_private_email
|
||||
|
||||
with patch.object(OAuth2Service, "get_auth_session", return_value=mock_session):
|
||||
login_via_oauth2("github", "iwriu", {"token": "ewrwerwer"}) # Dummy code and state token
|
||||
|
||||
def test_github_login_with_public_email(self):
|
||||
github_social_login_setup()
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_session.get.side_effect = github_response_for_public_email
|
||||
|
||||
with patch.object(OAuth2Service, "get_auth_session", return_value=mock_session):
|
||||
login_via_oauth2("github", "iwriu", {"token": "ewrwerwer"}) # Dummy code and state token
|
||||
|
||||
def test_normal_signup_and_github_login(self):
|
||||
github_social_login_setup()
|
||||
|
||||
if not frappe.db.exists("User", "githublogin@example.com"):
|
||||
user = frappe.get_doc({
|
||||
"doctype": "User",
|
||||
"email": "githublogin@example.com",
|
||||
"first_name": "GitHub Login"
|
||||
})
|
||||
user.save(ignore_permissions=True)
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_session.get.side_effect = github_response_for_login
|
||||
|
||||
with patch.object(OAuth2Service, "get_auth_session", return_value=mock_session):
|
||||
login_via_oauth2("github", "iwriu", {"token": "ewrwerwer"})
|
||||
|
||||
def make_social_login_key(**kwargs):
|
||||
kwargs["doctype"] = "Social Login Key"
|
||||
if not "provider_name" in kwargs:
|
||||
|
|
@ -34,3 +75,48 @@ def create_or_update_social_login_key():
|
|||
frappe.db.commit()
|
||||
|
||||
return social_login_key
|
||||
|
||||
def create_github_social_login_key():
|
||||
if frappe.db.exists("Social Login Key", "github"):
|
||||
return frappe.get_doc("Social Login Key", "github")
|
||||
else:
|
||||
provider_name = "GitHub"
|
||||
social_login_key = make_social_login_key(
|
||||
social_login_provider=provider_name
|
||||
)
|
||||
social_login_key.get_social_login_provider(provider_name, initialize=True)
|
||||
|
||||
# Dummy client_id and client_secret
|
||||
social_login_key.client_id = "h6htd6q"
|
||||
social_login_key.client_secret = "keoererk988ekkhf8w9e8ewrjhhkjer9889"
|
||||
social_login_key.insert(ignore_permissions=True)
|
||||
return social_login_key
|
||||
|
||||
def github_response_for_private_email(url, *args, **kwargs):
|
||||
if url == "user":
|
||||
return_value = {"login": "dummy_username", "id": "223342", "email": None, "first_name": "Github Private"}
|
||||
else:
|
||||
return_value = [{"email": "github@example.com", "primary": True, "verified": True}]
|
||||
|
||||
return MagicMock(status_code=200, json=MagicMock(return_value=return_value))
|
||||
|
||||
def github_response_for_public_email(url, *args, **kwargs):
|
||||
if url == "user":
|
||||
return_value = {"login": "dummy_username", "id": "223343", "email": "github_public@example.com", "first_name": "Github Public"}
|
||||
|
||||
return MagicMock(status_code=200, json=MagicMock(return_value=return_value))
|
||||
|
||||
def github_response_for_login(url, *args, **kwargs):
|
||||
if url == "user":
|
||||
return_value = {"login": "dummy_username", "id": "223346", "email": None, "first_name": "Github Login"}
|
||||
else:
|
||||
return_value = [{"email": "githublogin@example.com", "primary": True, "verified": True}]
|
||||
|
||||
return MagicMock(status_code=200, json=MagicMock(return_value=return_value))
|
||||
|
||||
def github_social_login_setup():
|
||||
set_request(path="/random")
|
||||
frappe.local.cookie_manager = CookieManager()
|
||||
frappe.local.login_manager = LoginManager()
|
||||
|
||||
create_github_social_login_key()
|
||||
|
|
|
|||
|
|
@ -81,9 +81,6 @@ class BaseDocument(object):
|
|||
if hasattr(self, "__setup__"):
|
||||
self.__setup__()
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.get(key) if hasattr(self, key) else frappe.throw(msg=key, exc=KeyError)
|
||||
|
||||
@property
|
||||
def meta(self):
|
||||
if not getattr(self, "_meta", None):
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from frappe import _
|
|||
from frappe.utils import now_datetime, cint, cstr
|
||||
import re
|
||||
from frappe.model import log_types
|
||||
from frappe.query_builder import DocType
|
||||
|
||||
|
||||
def set_new_name(doc):
|
||||
|
|
@ -194,7 +195,15 @@ def parse_naming_series(parts, doctype='', doc=''):
|
|||
|
||||
def getseries(key, digits):
|
||||
# series created ?
|
||||
current = frappe.db.sql("SELECT `current` FROM `tabSeries` WHERE `name`=%s FOR UPDATE", (key,))
|
||||
# Using frappe.qb as frappe.get_values does not allow order_by=None
|
||||
series = DocType("Series")
|
||||
current = (
|
||||
frappe.qb.from_(series)
|
||||
.where(series.name == key)
|
||||
.for_update()
|
||||
.select("current")
|
||||
).run()
|
||||
|
||||
if current and current[0][0] is not None:
|
||||
current = current[0][0]
|
||||
# yes, update it
|
||||
|
|
@ -260,7 +269,13 @@ def revert_series_if_last(key, name, doc=None):
|
|||
prefix = parse_naming_series(prefix.split('.'), doc=doc)
|
||||
|
||||
count = cint(name.replace(prefix, ""))
|
||||
current = frappe.db.sql("SELECT `current` FROM `tabSeries` WHERE `name`=%s FOR UPDATE", (prefix,))
|
||||
series = DocType("Series")
|
||||
current = (
|
||||
frappe.qb.from_(series)
|
||||
.where(series.name == prefix)
|
||||
.for_update()
|
||||
.select("current")
|
||||
).run()
|
||||
|
||||
if current and current[0][0]==count:
|
||||
frappe.db.sql("UPDATE `tabSeries` SET `current` = `current` - 1 WHERE `name`=%s", prefix)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from frappe.model.naming import validate_name
|
|||
from frappe.model.utils.user_settings import sync_user_settings, update_user_settings_data
|
||||
from frappe.utils import cint
|
||||
from frappe.utils.password import rename_password
|
||||
from frappe.query_builder import Field
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -191,8 +192,14 @@ def update_autoname_field(doctype, new, meta):
|
|||
|
||||
def validate_rename(doctype, new, meta, merge, force, ignore_permissions):
|
||||
# using for update so that it gets locked and someone else cannot edit it while this rename is going on!
|
||||
exists = frappe.db.sql("select name from `tab{doctype}` where name=%s for update".format(doctype=doctype), new)
|
||||
exists = exists[0][0] if exists else None
|
||||
exists = (
|
||||
frappe.qb.from_(doctype)
|
||||
.where(Field("name") == new)
|
||||
.for_update()
|
||||
.select("name")
|
||||
.run(pluck=True)
|
||||
)
|
||||
exists = exists[0] if exists else None
|
||||
|
||||
if merge and not exists:
|
||||
frappe.msgprint(_("{0} {1} does not exist, select a new target to merge").format(doctype, new), raise_exception=1)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ Integrations
|
|||
Printing
|
||||
Contacts
|
||||
Data Migration
|
||||
Chat
|
||||
Social
|
||||
Automation
|
||||
Event Streaming
|
||||
Event Streaming
|
||||
|
|
@ -183,5 +183,6 @@ frappe.patches.v13_0.jinja_hook
|
|||
frappe.patches.v13_0.update_notification_channel_if_empty
|
||||
frappe.patches.v14_0.drop_data_import_legacy
|
||||
frappe.patches.v14_0.rename_cancelled_documents
|
||||
frappe.patches.v14_0.update_workspace2 # 25.08.2021
|
||||
frappe.patches.v14_0.copy_mail_data #08.03.21
|
||||
frappe.patches.v14_0.copy_mail_data #08.03.21
|
||||
frappe.patches.v14_0.update_workspace2 # 20.09.2021
|
||||
frappe.patches.v14_0.update_github_endpoints #08-11-2021
|
||||
|
|
|
|||
10
frappe/patches/v14_0/update_github_endpoints.py
Normal file
10
frappe/patches/v14_0/update_github_endpoints.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import frappe
|
||||
import json
|
||||
|
||||
def execute():
|
||||
if frappe.db.exists("Social Login Key", "github"):
|
||||
frappe.db.set_value("Social Login Key", "github", "auth_url_data",
|
||||
json.dumps({
|
||||
"scope": "user:email"
|
||||
})
|
||||
)
|
||||
|
|
@ -1 +0,0 @@
|
|||
import "./frappe/chat";
|
||||
|
|
@ -99,7 +99,6 @@ import "./frappe/query_string.js";
|
|||
|
||||
// import "./frappe/ui/comment.js";
|
||||
|
||||
import "./frappe/chat.js";
|
||||
import "./frappe/utils/energy_point_utils.js";
|
||||
import "./frappe/utils/dashboard_utils.js";
|
||||
import "./frappe/ui/chart.js";
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,461 +0,0 @@
|
|||
// Author - Achilles Rasquinha <achilles@frappe.io>
|
||||
// http://codeguide.co - @mdo (Author of Bootstrap)
|
||||
|
||||
@import "../css/font-awesome.css";
|
||||
@import "../css/octicons/octicons.css";
|
||||
|
||||
// Typography
|
||||
@font-weight-bold: 700;
|
||||
@font-weight-heavy: 900;
|
||||
|
||||
@chat-toggle-height: 40px;
|
||||
|
||||
@fab-box-shadow: 0 5px 15px rgba(0, 0, 0, .25);
|
||||
@fab-size: 48px;
|
||||
@fab-size-lg: 56px;
|
||||
@fab-margin: 20px;
|
||||
|
||||
@chat-popper-margin: @fab-margin;
|
||||
@chat-popper-panel-width: 350px;
|
||||
@chat-popper-panel-height: 500px;
|
||||
// z-index greater than FAB, lesser than modal.
|
||||
@chat-popper-z-index: 1035;
|
||||
|
||||
// BS modal's box-shadow
|
||||
@chat-popper-panel-box-shadow: @fab-box-shadow;
|
||||
|
||||
// https://github.com/twbs/bootstrap/blob/v3.3.7/less/variables.less#L278
|
||||
// Keep z-index of the ChatPopper higher than others, lower than modal background.
|
||||
|
||||
@chat-room-list-content-max-width: 180px;
|
||||
|
||||
@chat-form-font-size: 12px;
|
||||
@chat-form-menu-border-radius: 4px;
|
||||
@chat-form-list-group-height: 150px; // Hints
|
||||
|
||||
// Typography
|
||||
.font-bold { font-weight: @font-weight-bold; }
|
||||
.font-heavy { font-weight: @font-weight-heavy; }
|
||||
|
||||
// Utilities
|
||||
.cursor-pointer { cursor: pointer; }
|
||||
|
||||
// Hacks and Fixes
|
||||
// suggested by rushabh@frappe.io. Thanks, Rushabh!
|
||||
// .avatar { padding: 2px; }
|
||||
|
||||
.frappe-fab
|
||||
{
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-radius: 50%;
|
||||
box-shadow: @fab-box-shadow;
|
||||
margin: @fab-margin;
|
||||
width: @fab-size;
|
||||
height: @fab-size;
|
||||
|
||||
&.frappe-fab-lg
|
||||
{
|
||||
width: @fab-size-lg;
|
||||
height: @fab-size-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar
|
||||
{
|
||||
.frappe-chat-toggle
|
||||
{
|
||||
height: @chat-toggle-height;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.octicon { margin-top: 3px; } // Hack, somewhat.
|
||||
}
|
||||
|
||||
.frappe-chat
|
||||
{
|
||||
& > .frappe-chat-popper
|
||||
{
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
margin: @chat-popper-margin;
|
||||
z-index: @chat-popper-z-index;
|
||||
|
||||
& > .frappe-chat-popper-collapse
|
||||
{
|
||||
& > .panel
|
||||
{
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: @chat-popper-panel-width;
|
||||
height: @chat-popper-panel-height;
|
||||
box-shadow: @chat-popper-panel-box-shadow;
|
||||
|
||||
.vcenter
|
||||
{
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.panel-heading
|
||||
{
|
||||
.panel-title
|
||||
{
|
||||
.media-heading
|
||||
{
|
||||
font-size: 12px;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.media-left {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.media-subtitle
|
||||
{
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.frappe-chat-action-bar
|
||||
{
|
||||
form
|
||||
{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-action
|
||||
{
|
||||
margin-left: 5px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.frappe-chat-room-list
|
||||
{
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 0 1px 0 1px;
|
||||
|
||||
& > li > a
|
||||
{
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
|
||||
.media
|
||||
{
|
||||
.media-heading, .media-subtitle
|
||||
{
|
||||
max-width: @chat-room-list-content-max-width;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > .panel.panel-bg
|
||||
{
|
||||
background-size: 350px 500px;
|
||||
background-image: url(/assets/frappe/images/chat/wallpaper-default.jpg);
|
||||
}
|
||||
|
||||
& > .panel.panel-span
|
||||
{
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
overflow: auto;
|
||||
border-radius: 0px;
|
||||
|
||||
.panel-heading
|
||||
{
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel
|
||||
{
|
||||
margin-bottom: 0px !important;
|
||||
|
||||
.chat-form
|
||||
{
|
||||
.form-control
|
||||
{
|
||||
font-size: @chat-form-font-size;
|
||||
}
|
||||
|
||||
.dropdown-menu
|
||||
{
|
||||
border-radius: @chat-form-menu-border-radius;
|
||||
}
|
||||
|
||||
// Hints
|
||||
.hint-list.list-group
|
||||
{
|
||||
margin: 0px;
|
||||
max-height: @chat-form-list-group-height;
|
||||
overflow-y: auto;
|
||||
|
||||
.hint-list-item.list-group-item:first-child, .hint-list-item.list-group-item:last-child
|
||||
{
|
||||
border-radius: 0px !important;
|
||||
|
||||
a { text-decoration: none }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@chat-color-grey: #8D99A6;
|
||||
|
||||
@chat-base-font-size: 12px;
|
||||
@chat-base-font-size-lg: 14px;
|
||||
|
||||
@chat-base-spacing: 5px;
|
||||
|
||||
// ChatForm
|
||||
@chat-form-border: 1px solid #D1D8DD;
|
||||
|
||||
// ChatList
|
||||
@chat-list-bg-color: #FAFBFC;
|
||||
|
||||
// ChatList.Item
|
||||
@chat-list-item-padding: @chat-base-spacing @chat-base-spacing * 2;
|
||||
|
||||
// ChatBubble
|
||||
@chat-bubble-padding: @chat-base-spacing @chat-base-spacing * 2;
|
||||
@chat-bubble-min-width: 25%;
|
||||
@chat-bubble-max-width: 75%;
|
||||
|
||||
@chat-bubble-box-shadow: 0px 0.1px 0.5px 0px rgba(0,0,0,0.5);
|
||||
|
||||
@chat-bubble-border-size: 1px;
|
||||
@chat-bubble-border-radius: @chat-base-spacing;
|
||||
|
||||
@chat-bubble-l-color: #EBEFF2;
|
||||
@chat-bubble-r-color: #EBF7CF;
|
||||
|
||||
@chat-bubble-l-groupable-margin-left: 40px;
|
||||
|
||||
@chat-bubble-author-font-size: @chat-base-font-size;
|
||||
|
||||
@chat-bubble-content-margin-bottom: @chat-base-spacing;
|
||||
|
||||
@chat-bubble-meta-font-size: @chat-base-spacing * 2;
|
||||
|
||||
@chat-bubble-check-font-size: @chat-base-font-size;
|
||||
|
||||
.frappe-chat-popper-collapse
|
||||
{
|
||||
& > .panel
|
||||
{
|
||||
& > .panel-heading
|
||||
{
|
||||
padding: @chat-base-spacing @chat-base-spacing * 2;
|
||||
|
||||
.btn-back
|
||||
{
|
||||
margin-right: @chat-base-spacing;
|
||||
}
|
||||
|
||||
.avatar
|
||||
{
|
||||
width: 32px; height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.chat-room-footer
|
||||
{
|
||||
.chat-form
|
||||
{
|
||||
border-top: @chat-form-border;
|
||||
|
||||
.input-group-btn
|
||||
{
|
||||
.btn
|
||||
{
|
||||
background: white;
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-control
|
||||
{
|
||||
line-height: 27px; // HACK: Makes input and placeholder centered within textarea. Also takes care of the input-btn
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
resize: none;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fa
|
||||
{
|
||||
font-size: @chat-base-font-size-lg;
|
||||
transition: color 0.5s; // Change, with grace. :)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.chat-list
|
||||
{
|
||||
height: 100%;
|
||||
// background: @chat-list-bg-color;
|
||||
overflow-y: scroll;
|
||||
|
||||
.chat-list-item
|
||||
{
|
||||
.avatar
|
||||
{
|
||||
vertical-align: top;
|
||||
|
||||
.standard-image
|
||||
{
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-pointer;
|
||||
|
||||
border: none !important;
|
||||
padding: @chat-list-item-padding;
|
||||
background: transparent;
|
||||
|
||||
.chat-bubble
|
||||
{
|
||||
max-width: @chat-bubble-max-width;
|
||||
display: inline-block;
|
||||
padding: @chat-bubble-padding;
|
||||
border-radius: @chat-bubble-border-radius;
|
||||
|
||||
-webkit-box-shadow: @chat-bubble-box-shadow;
|
||||
-moz-box-shadow: @chat-bubble-box-shadow;
|
||||
box-shadow: @chat-bubble-box-shadow;
|
||||
|
||||
@media (max-width : 768px) {
|
||||
min-width: @chat-bubble-min-width;
|
||||
}
|
||||
|
||||
&.chat-bubble-l
|
||||
{
|
||||
&.chat-groupable
|
||||
{
|
||||
margin-left: @chat-bubble-l-groupable-margin-left;
|
||||
}
|
||||
|
||||
// background-color: @chat-bubble-l-color;
|
||||
background-color: white;
|
||||
|
||||
.chat-bubble-meta
|
||||
{
|
||||
& > .chat-bubble-creation, & > .chat-bubble-check i
|
||||
{
|
||||
color: darken(@chat-bubble-l-color, 50%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.chat-bubble-r
|
||||
{
|
||||
text-align: right;
|
||||
background-color: @chat-bubble-r-color;
|
||||
|
||||
.chat-bubble-meta
|
||||
{
|
||||
& > .chat-bubble-creation, & > .chat-bubble-check i
|
||||
{
|
||||
color: darken(@chat-bubble-r-color, 50%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-bubble-author
|
||||
{
|
||||
font-size: @chat-bubble-author-font-size;
|
||||
|
||||
a
|
||||
{
|
||||
.font-bold;
|
||||
|
||||
text-decoration: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-bubble-content
|
||||
{
|
||||
margin-bottom: @chat-bubble-content-margin-bottom;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.chat-bubble-meta
|
||||
{
|
||||
font-size: @chat-bubble-meta-font-size;
|
||||
|
||||
& > .chat-bubble-check
|
||||
{
|
||||
margin-left: @chat-base-spacing;
|
||||
|
||||
i
|
||||
{
|
||||
font-size: @chat-bubble-check-font-size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-list-notification
|
||||
{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chat-list-notification-content
|
||||
{
|
||||
color: white;
|
||||
background-color: #8D99A6;
|
||||
display: inline-block;
|
||||
/* padding: 5px; */
|
||||
border-radius: 20px;
|
||||
opacity: 0.5;
|
||||
font-size: 10px;
|
||||
padding: 5px;
|
||||
// background-color: white;
|
||||
}
|
||||
|
||||
// v12 fixes for visitor chat
|
||||
.panel-default > .panel-heading {
|
||||
background-color: #f7fafc;
|
||||
border-color: #ced5db;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
resize: none;
|
||||
height: 3.2em;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.nav-stacked > li + li {
|
||||
margin-top: 0px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,7 +1,14 @@
|
|||
from pypika.functions import *
|
||||
from pypika.terms import Function
|
||||
from frappe.query_builder.utils import ImportMapper, db_type_is
|
||||
from frappe.query_builder.custom import GROUP_CONCAT, STRING_AGG, MATCH, TO_TSVECTOR
|
||||
|
||||
|
||||
class Concat_ws(Function):
|
||||
def __init__(self, *terms, **kwargs):
|
||||
super(Concat_ws, self).__init__("CONCAT_WS", *terms, **kwargs)
|
||||
|
||||
|
||||
GroupConcat = ImportMapper(
|
||||
{
|
||||
db_type_is.MARIADB: GROUP_CONCAT,
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@
|
|||
window.dev_server = {{ dev_server }};
|
||||
window.socketio_port = {{ (frappe.socketio_port or 'null') }};
|
||||
window.show_language_picker = {{ show_language_picker or 'false' }};
|
||||
window.is_chat_enabled = {{ chat_enable }};
|
||||
</script>
|
||||
</head>
|
||||
<body frappe-session-status="{{ 'logged-in' if frappe.session.user != 'Guest' else 'logged-out'}}" data-path="{{ path | e }}" {%- if template and template.endswith('.md') %} frappe-content-type="markdown" {%- endif %} class="{{ body_class or ''}}">
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from frappe.utils.testutils import clear_custom_fields
|
|||
from frappe.query_builder import Field
|
||||
|
||||
from .test_query_builder import run_only_if, db_type_is
|
||||
from frappe.query_builder.functions import Concat_ws
|
||||
|
||||
|
||||
class TestDB(unittest.TestCase):
|
||||
|
|
@ -33,6 +34,8 @@ class TestDB(unittest.TestCase):
|
|||
self.assertEqual(frappe.db.sql("""SELECT name FROM `tabUser` WHERE name >= 't' ORDER BY MODIFIED DESC""")[0][0],
|
||||
frappe.db.get_value("User", {"name": [">=", "t"]}))
|
||||
|
||||
self.assertIn("concat_ws", frappe.db.get_value("User", filters={"name": "Administrator"}, fieldname=Concat_ws(" ", "LastName"), run=False).lower())
|
||||
|
||||
def test_set_value(self):
|
||||
todo1 = frappe.get_doc(dict(doctype='ToDo', description = 'test_set_value 1')).insert()
|
||||
todo2 = frappe.get_doc(dict(doctype='ToDo', description = 'test_set_value 2')).insert()
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from typing import Generator, Iterable
|
|||
from urllib.parse import quote, urlparse
|
||||
from werkzeug.test import Client
|
||||
from redis.exceptions import ConnectionError
|
||||
from collections.abc import MutableMapping, MutableSequence, Sequence
|
||||
|
||||
import frappe
|
||||
# utility functions like cint, int, flt, etc.
|
||||
|
|
@ -861,3 +862,31 @@ def groupby_metric(iterable: typing.Dict[str, list], key: str):
|
|||
|
||||
def get_table_name(table_name: str) -> str:
|
||||
return f"tab{table_name}" if not table_name.startswith("__") else table_name
|
||||
|
||||
def squashify(what):
|
||||
if isinstance(what, Sequence) and len(what) == 1:
|
||||
return what[0]
|
||||
|
||||
return what
|
||||
|
||||
def safe_json_loads(*args):
|
||||
results = []
|
||||
|
||||
for arg in args:
|
||||
try:
|
||||
arg = json.loads(arg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
results.append(arg)
|
||||
|
||||
return squashify(results)
|
||||
|
||||
def dictify(arg):
|
||||
if isinstance(arg, MutableSequence):
|
||||
for i, a in enumerate(arg):
|
||||
arg[i] = dictify(a)
|
||||
elif isinstance(arg, MutableMapping):
|
||||
arg = frappe._dict(arg)
|
||||
|
||||
return arg
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from datetime import datetime
|
|||
from glob import glob
|
||||
from shutil import which
|
||||
|
||||
|
||||
# imports - third party imports
|
||||
import click
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ import click
|
|||
import frappe
|
||||
from frappe import _, conf
|
||||
from frappe.utils import get_file_size, get_url, now, now_datetime, cint
|
||||
from frappe.utils.password import get_encryption_key
|
||||
|
||||
# backup variable for backwards compatibility
|
||||
verbose = False
|
||||
|
|
@ -197,6 +199,9 @@ class BackupGenerator:
|
|||
if not ignore_files:
|
||||
self.backup_files()
|
||||
|
||||
if frappe.get_system_settings("encrypt_backup"):
|
||||
self.backup_encryption()
|
||||
|
||||
else:
|
||||
self.backup_path_files = last_file
|
||||
self.backup_path_db = last_db
|
||||
|
|
@ -206,11 +211,13 @@ class BackupGenerator:
|
|||
def set_backup_file_name(self):
|
||||
partial = "-partial" if self.partial else ""
|
||||
ext = "tgz" if self.compress_files else "tar"
|
||||
enc = "-enc" if frappe.get_system_settings("encrypt_backup") else ""
|
||||
|
||||
for_conf = f"{self.todays_date}-{self.site_slug}-site_config_backup.json"
|
||||
for_db = f"{self.todays_date}-{self.site_slug}{partial}-database.sql.gz"
|
||||
for_public_files = f"{self.todays_date}-{self.site_slug}-files.{ext}"
|
||||
for_private_files = f"{self.todays_date}-{self.site_slug}-private-files.{ext}"
|
||||
|
||||
for_conf = f"{self.todays_date}-{self.site_slug}-site_config_backup{enc}.json"
|
||||
for_db = f"{self.todays_date}-{self.site_slug}{partial}-database{enc}.sql.gz"
|
||||
for_public_files = f"{self.todays_date}-{self.site_slug}-files{enc}.{ext}"
|
||||
for_private_files = f"{self.todays_date}-{self.site_slug}-private-files{enc}.{ext}"
|
||||
backup_path = self.backup_path or get_backup_path()
|
||||
|
||||
if not self.backup_path_conf:
|
||||
|
|
@ -222,15 +229,46 @@ class BackupGenerator:
|
|||
if not self.backup_path_private_files:
|
||||
self.backup_path_private_files = os.path.join(backup_path, for_private_files)
|
||||
|
||||
def backup_encryption(self):
|
||||
"""
|
||||
Encrypt all the backups created using gpg.
|
||||
"""
|
||||
paths = (self.backup_path_db, self.backup_path_files, self.backup_path_private_files)
|
||||
for path in paths:
|
||||
if os.path.exists(path):
|
||||
cmd_string = (
|
||||
"gpg --yes --passphrase {passphrase} --pinentry-mode loopback -c {filelocation}"
|
||||
)
|
||||
try:
|
||||
command = cmd_string.format(
|
||||
passphrase=get_encryption_key(),
|
||||
filelocation=path,
|
||||
)
|
||||
|
||||
frappe.utils.execute_in_shell(command)
|
||||
os.rename(path + ".gpg", path)
|
||||
|
||||
except Exception as err:
|
||||
print(err)
|
||||
click.secho("Error occurred during encryption. Files are stored without encryption.", fg="red")
|
||||
|
||||
def get_recent_backup(self, older_than, partial=False):
|
||||
backup_path = get_backup_path()
|
||||
|
||||
file_type_slugs = {
|
||||
"database": "*-{{}}-{}database.sql.gz".format('*' if partial else ''),
|
||||
"public": "*-{}-files.tar",
|
||||
"private": "*-{}-private-files.tar",
|
||||
"config": "*-{}-site_config_backup.json",
|
||||
}
|
||||
if not frappe.get_system_settings("encrypt_backup"):
|
||||
file_type_slugs = {
|
||||
"database": "*-{{}}-{}database.sql.gz".format('*' if partial else ''),
|
||||
"public": "*-{}-files.tar",
|
||||
"private": "*-{}-private-files.tar",
|
||||
"config": "*-{}-site_config_backup.json",
|
||||
}
|
||||
else:
|
||||
file_type_slugs = {
|
||||
"database": "*-{{}}-{}database.enc.sql.gz".format('*' if partial else ''),
|
||||
"public": "*-{}-files.enc.tar",
|
||||
"private": "*-{}-private-files.enc.tar",
|
||||
"config": "*-{}-site_config_backup.json",
|
||||
}
|
||||
|
||||
def backup_time(file_path):
|
||||
file_name = file_path.split(os.sep)[-1]
|
||||
|
|
@ -613,6 +651,51 @@ def get_backup_path():
|
|||
backup_path = frappe.utils.get_site_path(conf.get("backup_path", "private/backups"))
|
||||
return backup_path
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_backup_encryption_key():
|
||||
return frappe.local.conf.encryption_key
|
||||
|
||||
class Backup:
|
||||
def __init__(self, file_path):
|
||||
self.file_path = file_path
|
||||
|
||||
def backup_decryption(self,passphrase):
|
||||
"""
|
||||
Decrypts backup at the given path using the passphrase.
|
||||
"""
|
||||
if not os.path.exists(self.file_path):
|
||||
print("Invalid path", self.file_path)
|
||||
return
|
||||
else:
|
||||
os.rename(self.file_path, self.file_path + ".gpg")
|
||||
file_path = self.file_path + ".gpg"
|
||||
|
||||
cmd_string = (
|
||||
"gpg --yes --passphrase {passphrase} --pinentry-mode loopback -o {decrypted_file} -d {file_location}"
|
||||
)
|
||||
command = cmd_string.format(
|
||||
passphrase=passphrase,
|
||||
file_location=file_path,
|
||||
decrypted_file=file_path.rstrip(".gpg"),
|
||||
)
|
||||
frappe.utils.execute_in_shell(command)
|
||||
|
||||
|
||||
def decryption_rollback(self):
|
||||
"""
|
||||
Checks if the decrypted file exists at the given path.
|
||||
if exists
|
||||
Renames the orginal encrypted file.
|
||||
else
|
||||
Removes the decrypted file and rename the original file.
|
||||
"""
|
||||
if os.path.exists(self.file_path + ".gpg"):
|
||||
if os.path.exists(self.file_path):
|
||||
os.remove(self.file_path)
|
||||
if os.path.exists(self.file_path.rstrip(".gz")):
|
||||
os.remove(self.file_path.rstrip(".gz"))
|
||||
os.rename(self.file_path + ".gpg", self.file_path)
|
||||
|
||||
|
||||
def backup(
|
||||
with_files=False,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ dateformats = {
|
|||
'dd-mmm-yyyy': '%d-%b-%Y', # numbers app format
|
||||
'dd/mm/yyyy': '%d/%m/%Y',
|
||||
'dd.mm.yyyy': '%d.%m.%Y',
|
||||
'dd.mm.yy': '%d.%m.%y',
|
||||
'dd-mm-yyyy': '%d-%m-%Y',
|
||||
"dd/mm/yy": "%d/%m/%y",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import now
|
||||
from frappe.query_builder import DocType, Order
|
||||
|
||||
class NestedSetRecursionError(frappe.ValidationError): pass
|
||||
class NestedSetMultipleRootsError(frappe.ValidationError): pass
|
||||
|
|
@ -23,25 +23,25 @@ class NestedSetInvalidMergeError(frappe.ValidationError): pass
|
|||
# called in the on_update method
|
||||
def update_nsm(doc):
|
||||
# get fields, data from the DocType
|
||||
opf = 'old_parent'
|
||||
pf = "parent_" + frappe.scrub(doc.doctype)
|
||||
old_parent_field = 'old_parent'
|
||||
parent_field = "parent_" + frappe.scrub(doc.doctype)
|
||||
|
||||
if hasattr(doc,'nsm_parent_field'):
|
||||
pf = doc.nsm_parent_field
|
||||
parent_field = doc.nsm_parent_field
|
||||
if hasattr(doc,'nsm_oldparent_field'):
|
||||
opf = doc.nsm_oldparent_field
|
||||
old_parent_field = doc.nsm_oldparent_field
|
||||
|
||||
p, op = doc.get(pf) or None, doc.get(opf) or None
|
||||
parent, old_parent = doc.get(parent_field) or None, doc.get(old_parent_field) or None
|
||||
|
||||
# has parent changed (?) or parent is None (root)
|
||||
if not doc.lft and not doc.rgt:
|
||||
update_add_node(doc, p or '', pf)
|
||||
elif op != p:
|
||||
update_move_node(doc, pf)
|
||||
update_add_node(doc, parent or '', parent_field)
|
||||
elif old_parent != parent:
|
||||
update_move_node(doc, parent_field)
|
||||
|
||||
# set old parent
|
||||
doc.set(opf, p)
|
||||
frappe.db.set_value(doc.doctype, doc.name, opf, p or '', update_modified=False)
|
||||
doc.set(old_parent_field, parent)
|
||||
frappe.db.set_value(doc.doctype, doc.name, old_parent_field, parent or '', update_modified=False)
|
||||
|
||||
doc.reload()
|
||||
|
||||
|
|
@ -50,8 +50,6 @@ def update_add_node(doc, parent, parent_field):
|
|||
insert a new node
|
||||
"""
|
||||
|
||||
n = now()
|
||||
|
||||
doctype = doc.doctype
|
||||
name = doc.name
|
||||
|
||||
|
|
@ -68,23 +66,22 @@ def update_add_node(doc, parent, parent_field):
|
|||
right = right or 1
|
||||
|
||||
# update all on the right
|
||||
frappe.db.sql("update `tab{0}` set rgt = rgt+2, modified=%s where rgt >= %s"
|
||||
.format(doctype), (n, right))
|
||||
frappe.db.sql("update `tab{0}` set lft = lft+2, modified=%s where lft >= %s"
|
||||
.format(doctype), (n, right))
|
||||
frappe.db.sql("update `tab{0}` set rgt = rgt+2 where rgt >= %s"
|
||||
.format(doctype), (right,))
|
||||
frappe.db.sql("update `tab{0}` set lft = lft+2 where lft >= %s"
|
||||
.format(doctype), (right,))
|
||||
|
||||
# update index of new node
|
||||
if frappe.db.sql("select * from `tab{0}` where lft=%s or rgt=%s".format(doctype), (right, right+1)):
|
||||
frappe.msgprint(_("Nested set error. Please contact the Administrator."))
|
||||
raise Exception
|
||||
|
||||
frappe.db.sql("update `tab{0}` set lft=%s, rgt=%s, modified=%s where name=%s".format(doctype),
|
||||
(right,right+1, n, name))
|
||||
frappe.db.sql("update `tab{0}` set lft=%s, rgt=%s where name=%s".format(doctype),
|
||||
(right,right+1, name))
|
||||
return right
|
||||
|
||||
|
||||
def update_move_node(doc, parent_field):
|
||||
n = now()
|
||||
parent = doc.get(parent_field)
|
||||
|
||||
if parent:
|
||||
|
|
@ -94,17 +91,17 @@ def update_move_node(doc, parent_field):
|
|||
validate_loop(doc.doctype, doc.name, new_parent.lft, new_parent.rgt)
|
||||
|
||||
# move to dark side
|
||||
frappe.db.sql("""update `tab{0}` set lft = -lft, rgt = -rgt, modified=%s
|
||||
where lft >= %s and rgt <= %s""".format(doc.doctype), (n, doc.lft, doc.rgt))
|
||||
frappe.db.sql("""update `tab{0}` set lft = -lft, rgt = -rgt
|
||||
where lft >= %s and rgt <= %s""".format(doc.doctype), (doc.lft, doc.rgt))
|
||||
|
||||
# shift left
|
||||
diff = doc.rgt - doc.lft + 1
|
||||
frappe.db.sql("""update `tab{0}` set lft = lft -%s, rgt = rgt - %s, modified=%s
|
||||
where lft > %s""".format(doc.doctype), (diff, diff, n, doc.rgt))
|
||||
frappe.db.sql("""update `tab{0}` set lft = lft -%s, rgt = rgt - %s
|
||||
where lft > %s""".format(doc.doctype), (diff, diff, doc.rgt))
|
||||
|
||||
# shift left rgts of ancestors whose only rgts must shift
|
||||
frappe.db.sql("""update `tab{0}` set rgt = rgt - %s, modified=%s
|
||||
where lft < %s and rgt > %s""".format(doc.doctype), (diff, n, doc.lft, doc.rgt))
|
||||
frappe.db.sql("""update `tab{0}` set rgt = rgt - %s
|
||||
where lft < %s and rgt > %s""".format(doc.doctype), (diff, doc.lft, doc.rgt))
|
||||
|
||||
if parent:
|
||||
new_parent = frappe.db.sql("""select lft, rgt from `tab%s`
|
||||
|
|
@ -112,17 +109,17 @@ def update_move_node(doc, parent_field):
|
|||
|
||||
|
||||
# set parent lft, rgt
|
||||
frappe.db.sql("""update `tab{0}` set rgt = rgt + %s, modified=%s
|
||||
where name = %s""".format(doc.doctype), (diff, n, parent))
|
||||
frappe.db.sql("""update `tab{0}` set rgt = rgt + %s
|
||||
where name = %s""".format(doc.doctype), (diff, parent))
|
||||
|
||||
# shift right at new parent
|
||||
frappe.db.sql("""update `tab{0}` set lft = lft + %s, rgt = rgt + %s, modified=%s
|
||||
where lft > %s""".format(doc.doctype), (diff, diff, n, new_parent.rgt))
|
||||
frappe.db.sql("""update `tab{0}` set lft = lft + %s, rgt = rgt + %s
|
||||
where lft > %s""".format(doc.doctype), (diff, diff, new_parent.rgt))
|
||||
|
||||
# shift right rgts of ancestors whose only rgts must shift
|
||||
frappe.db.sql("""update `tab{0}` set rgt = rgt + %s, modified=%s
|
||||
frappe.db.sql("""update `tab{0}` set rgt = rgt + %s
|
||||
where lft < %s and rgt > %s""".format(doc.doctype),
|
||||
(diff, n, new_parent.lft, new_parent.rgt))
|
||||
(diff, new_parent.lft, new_parent.rgt))
|
||||
|
||||
|
||||
new_diff = new_parent.rgt - doc.lft
|
||||
|
|
@ -132,8 +129,8 @@ def update_move_node(doc, parent_field):
|
|||
new_diff = max_rgt + 1 - doc.lft
|
||||
|
||||
# bring back from dark side
|
||||
frappe.db.sql("""update `tab{0}` set lft = -lft + %s, rgt = -rgt + %s, modified=%s
|
||||
where lft < 0""".format(doc.doctype), (new_diff, new_diff, n))
|
||||
frappe.db.sql("""update `tab{0}` set lft = -lft + %s, rgt = -rgt + %s
|
||||
where lft < 0""".format(doc.doctype), (new_diff, new_diff))
|
||||
|
||||
@frappe.whitelist()
|
||||
def rebuild_tree(doctype, parent_field):
|
||||
|
|
@ -146,10 +143,21 @@ def rebuild_tree(doctype, parent_field):
|
|||
frappe.only_for('System Manager')
|
||||
|
||||
# get all roots
|
||||
right = 1
|
||||
table = DocType(doctype)
|
||||
column = getattr(table, parent_field)
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(table)
|
||||
.where(
|
||||
(column == "") | (column.isnull())
|
||||
)
|
||||
.orderby(table.name, order=Order.asc)
|
||||
.select(table.name)
|
||||
).run()
|
||||
|
||||
frappe.db.auto_commit_on_many_writes = 1
|
||||
|
||||
right = 1
|
||||
result = frappe.db.sql("SELECT name FROM `tab%s` WHERE `%s`='' or `%s` IS NULL ORDER BY name ASC" % (doctype, parent_field, parent_field))
|
||||
for r in result:
|
||||
right = rebuild_node(doctype, r[0], right, parent_field)
|
||||
|
||||
|
|
@ -159,22 +167,23 @@ def rebuild_node(doctype, parent, left, parent_field):
|
|||
"""
|
||||
reset lft, rgt and recursive call for all children
|
||||
"""
|
||||
from frappe.utils import now
|
||||
n = now()
|
||||
|
||||
# the right value of this node is the left value + 1
|
||||
right = left+1
|
||||
|
||||
# get all children of this node
|
||||
result = frappe.db.sql("SELECT name FROM `tab{0}` WHERE `{1}`=%s"
|
||||
.format(doctype, parent_field), (parent))
|
||||
table = DocType(doctype)
|
||||
column = getattr(table, parent_field)
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(table).where(column == parent).select(table.name)
|
||||
).run()
|
||||
|
||||
for r in result:
|
||||
right = rebuild_node(doctype, r[0], right, parent_field)
|
||||
|
||||
# we've got the left value, and now that we've processed
|
||||
# the children of this node we also know the right value
|
||||
frappe.db.sql("""UPDATE `tab{0}` SET lft=%s, rgt=%s, modified=%s
|
||||
WHERE name=%s""".format(doctype), (left,right,n,parent))
|
||||
frappe.db.set_value(doctype, parent, {"lft": left, "rgt": right}, for_update=False, update_modified=False)
|
||||
|
||||
#return the right value of this node + 1
|
||||
return right+1
|
||||
|
|
|
|||
|
|
@ -138,8 +138,14 @@ def get_info_via_oauth(provider, code, decoder=None, id_token=False):
|
|||
else:
|
||||
api_endpoint = oauth2_providers[provider].get("api_endpoint")
|
||||
api_endpoint_args = oauth2_providers[provider].get("api_endpoint_args")
|
||||
|
||||
info = session.get(api_endpoint, params=api_endpoint_args).json()
|
||||
|
||||
if provider == "github" and not info.get("email"):
|
||||
emails = session.get("/user/emails", params=api_endpoint_args).json()
|
||||
email_dict = list(filter(lambda x: x.get("primary"), emails))[0]
|
||||
info["email"] = email_dict.get("email")
|
||||
|
||||
if not (info.get("email_verified") or info.get("email")):
|
||||
frappe.throw(_("Email not verified with {0}").format(provider.title()))
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ from frappe.utils import cint
|
|||
from frappe.boot import get_allowed_reports
|
||||
from frappe.permissions import get_roles, get_valid_perms
|
||||
from frappe.core.doctype.domain_settings.domain_settings import get_active_modules
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.functions import Concat_ws
|
||||
|
||||
class UserPermissions:
|
||||
"""
|
||||
|
|
@ -208,8 +210,13 @@ class UserPermissions:
|
|||
return get_allowed_reports()
|
||||
|
||||
def get_user_fullname(user):
|
||||
fullname = frappe.db.sql("SELECT CONCAT_WS(' ', first_name, last_name) FROM `tabUser` WHERE name=%s", (user,))
|
||||
return fullname and fullname[0][0] or ''
|
||||
user_doctype = DocType("User")
|
||||
fullname = frappe.get_value(
|
||||
user_doctype,
|
||||
filters={"name": user},
|
||||
fieldname=Concat_ws(" ", user_doctype.first_name, user_doctype.last_name),
|
||||
)
|
||||
return fullname or ''
|
||||
|
||||
def get_fullname_and_avatar(user):
|
||||
first_name, last_name, avatar, name = frappe.db.get_value("User",
|
||||
|
|
|
|||
|
|
@ -63,14 +63,7 @@
|
|||
"subdomain",
|
||||
"head_html",
|
||||
"robots_txt",
|
||||
"route_redirects",
|
||||
"section_break_39",
|
||||
"chat_enable",
|
||||
"chat_enable_from",
|
||||
"chat_enable_to",
|
||||
"chat_room_name",
|
||||
"chat_welcome_message",
|
||||
"chat_operators"
|
||||
"route_redirects"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -266,51 +259,6 @@
|
|||
"fieldtype": "Code",
|
||||
"label": "Robots.txt"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_39",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Chat"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "chat_enable",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Chat"
|
||||
},
|
||||
{
|
||||
"depends_on": "chat_enable",
|
||||
"fieldname": "chat_enable_from",
|
||||
"fieldtype": "Time",
|
||||
"label": "From"
|
||||
},
|
||||
{
|
||||
"depends_on": "chat_enable",
|
||||
"fieldname": "chat_enable_to",
|
||||
"fieldtype": "Time",
|
||||
"label": "To"
|
||||
},
|
||||
{
|
||||
"default": "Support",
|
||||
"depends_on": "chat_enable",
|
||||
"fieldname": "chat_room_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Chat Room Name"
|
||||
},
|
||||
{
|
||||
"default": "Hi, how may I help you?",
|
||||
"depends_on": "chat_enable",
|
||||
"fieldname": "chat_welcome_message",
|
||||
"fieldtype": "Data",
|
||||
"label": "Welcome Message"
|
||||
},
|
||||
{
|
||||
"depends_on": "chat_enable",
|
||||
"fieldname": "chat_operators",
|
||||
"fieldtype": "Table",
|
||||
"label": "Chat Operators",
|
||||
"options": "Chat Room User"
|
||||
},
|
||||
{
|
||||
"fieldname": "route_redirects",
|
||||
"fieldtype": "Table",
|
||||
|
|
@ -470,4 +418,4 @@
|
|||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,8 +120,7 @@ def get_website_settings(context=None):
|
|||
"facebook_share", "google_plus_one", "twitter_share", "linked_in_share",
|
||||
"disable_signup", "hide_footer_signup", "head_html", "title_prefix",
|
||||
"navbar_template", "footer_template", "navbar_search", "enable_view_tracking",
|
||||
"footer_logo", "call_to_action", "call_to_action_url", "show_language_picker",
|
||||
"chat_enable"]:
|
||||
"footer_logo", "call_to_action", "call_to_action_url", "show_language_picker"]:
|
||||
if hasattr(settings, k):
|
||||
context[k] = settings.get(k)
|
||||
|
||||
|
|
|
|||
|
|
@ -633,17 +633,5 @@ $(document).on("page-change", function() {
|
|||
|
||||
frappe.ready(function() {
|
||||
frappe.show_language_picker();
|
||||
if (window.is_chat_enabled) {
|
||||
frappe.require([
|
||||
"/assets/frappe/node_modules/moment/min/moment-with-locales.min.js",
|
||||
"/assets/frappe/node_modules/moment-timezone/builds/moment-timezone-with-data.min.js",
|
||||
"chat.bundle.css",
|
||||
"/assets/frappe/js/lib/socket.io.min.js"
|
||||
], () => {
|
||||
frappe.require('chat.bundle.js', () => {
|
||||
frappe.chat.setup();
|
||||
});
|
||||
});
|
||||
}
|
||||
frappe.socketio.init(window.socketio_port);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue