* added doctypes, created frappe chat ui * added component layout with state-like abilities, added apis * updated user doctype, moved from state-like feature and component abstraction * added room component * fixed publish_realtime with after_commit = True * created room component and searchbar * minor fix * functional message parsing * update * Added Chat Profile * added chat message * more changes into chat room * fixed APIs, added client side scripting * added chat message attachements, more doc updates * Brand New UI with socket io room integration * completed socketio integration. off to room subscription and publish * realtime room update * raw update * initialized docs, added p2p connection for call tests * updated docs * added coverage, updated api for ease of use * raw commit * added test cases * Chat Room updates and new room creation * added chat group creation * added collapsible plugin * toggable room view * updated * [RAW] * updated UI for chat * Deleted Previous Chat Page * moved from frappe.Chat.Widget to frappe.Chat * modularized frappe-fab * added more docstrings * tried adding conversation tones * Added conversation_tones and refurbished chat popper * modified frappe.ui.Dialog, moved from AppBar to ActionBar, responsive for Mobile 💃 * moved RoomList item namespace * Configurable Desktop update, moved profile updates to on_update * added state change listeners * removed AppBar to ActionBar customizable 💃 * added destroy method * removed coverage, refactored group creation * Successful Chat Rooms and Group creation * sort rows based on last_message_timestamp or creation * added frappe._.compare * removed redundant less variables * Chat Room back button with custom routing and destroy methods * Added EmojiPicker * fixed multiple dialog render * setup quick access * added chat chime, functional chat message list updates at room list * deleted package-lock.json * realtime date updates * updated chat message list * functional message render and updates * added track seen * added typing status * updated typing status * valid typing statuses and quick search * Functional Quick Search * reverted fix * some more cleanup and promisifed * fixed hints close on click * updated fab boldness * close popper on click panel * close popper on click panel * reverted octicon-lg, fixed popper heading click * new frappe capture * removed webcamjs * added uploader and capture * removed chat FAB, added as notification instead * on message updateversion-14
@@ -1,3 +0,0 @@ | |||
{ | |||
"python.linting.pylintEnabled": false | |||
} |
@@ -89,8 +89,10 @@ def publish_realtime(event=None, message=None, room=None, | |||
room = get_user_room(user) | |||
elif doctype and docname: | |||
room = get_doc_room(doctype, docname) | |||
else: | |||
room = get_site_room() | |||
else: | |||
# frappe.chat | |||
room = get_chat_room(room) | |||
# end frappe.chat | |||
if after_commit: | |||
params = [event, message, room] | |||
@@ -110,7 +112,7 @@ def emit_via_redis(event, message, room): | |||
try: | |||
r.publish('events', frappe.as_json({'event': event, 'message': message, 'room': room})) | |||
except redis.exceptions.ConnectionError: | |||
# print frappe.get_traceback() | |||
# print(frappe.get_traceback()) | |||
pass | |||
def put_log(line_no, line, task_id=None): | |||
@@ -194,3 +196,10 @@ def get_site_room(): | |||
def get_task_progress_room(task_id): | |||
return "".join([frappe.local.site, ":task_progress:", task_id]) | |||
# frappe.chat | |||
def get_chat_room(room): | |||
room = ''.join([frappe.local.site, ":room:", room]) | |||
return room | |||
# end frappe.chat room |
@@ -154,7 +154,7 @@ function get_compiled_file(file, output_path, minify, force_compile) { | |||
function babelify(content, path, minify) { | |||
let presets = ['env']; | |||
var plugins = ['transform-object-rest-spread'] | |||
const plugins = ['transform-object-rest-spread'] | |||
// Minification doesn't work when loading Frappe Desk | |||
// Avoid for now, trace the error and come back. | |||
try { | |||
@@ -0,0 +1,8 @@ | |||
// Copyright (c) 2017, Frappe Technologies and contributors | |||
// For license information, please see license.txt | |||
frappe.ui.form.on('Chat Message', { | |||
refresh: function(frm) { | |||
} | |||
}); |
@@ -0,0 +1,245 @@ | |||
{ | |||
"allow_copy": 0, | |||
"allow_guest_to_view": 0, | |||
"allow_import": 0, | |||
"allow_rename": 0, | |||
"beta": 1, | |||
"creation": "2017-11-10 11:10:40.011099", | |||
"custom": 0, | |||
"docstatus": 0, | |||
"doctype": "DocType", | |||
"document_type": "", | |||
"editable_grid": 1, | |||
"engine": "InnoDB", | |||
"fields": [ | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "type", | |||
"fieldtype": "Select", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 1, | |||
"in_standard_filter": 0, | |||
"label": "Type", | |||
"length": 0, | |||
"no_copy": 0, | |||
"options": "Direct\nGroup\nVisitor", | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 1, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "user", | |||
"fieldtype": "Link", | |||
"hidden": 1, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "User", | |||
"length": 0, | |||
"no_copy": 0, | |||
"options": "User", | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 1, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "room", | |||
"fieldtype": "Link", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Room", | |||
"length": 0, | |||
"no_copy": 0, | |||
"options": "Chat Room", | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 1, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "content", | |||
"fieldtype": "Text", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Content", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 1, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "mentions", | |||
"fieldtype": "Code", | |||
"hidden": 1, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Mentions", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "urls", | |||
"fieldtype": "Data", | |||
"hidden": 1, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "URLs", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
} | |||
], | |||
"has_web_view": 0, | |||
"hide_heading": 0, | |||
"hide_toolbar": 0, | |||
"idx": 0, | |||
"image_view": 0, | |||
"in_create": 0, | |||
"is_submittable": 0, | |||
"issingle": 0, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-12-28 16:26:24.142473", | |||
"modified_by": "achilles@erpnext.com", | |||
"module": "Chat", | |||
"name": "Chat Message", | |||
"name_case": "", | |||
"owner": "arjun@gmail.com", | |||
"permissions": [ | |||
{ | |||
"amend": 0, | |||
"apply_user_permissions": 0, | |||
"cancel": 0, | |||
"create": 1, | |||
"delete": 1, | |||
"email": 1, | |||
"export": 1, | |||
"if_owner": 0, | |||
"import": 0, | |||
"permlevel": 0, | |||
"print": 1, | |||
"read": 1, | |||
"report": 1, | |||
"role": "System Manager", | |||
"set_user_permissions": 0, | |||
"share": 1, | |||
"submit": 0, | |||
"write": 1 | |||
} | |||
], | |||
"quick_entry": 1, | |||
"read_only": 0, | |||
"read_only_onload": 0, | |||
"show_name_in_global_search": 0, | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"track_changes": 1, | |||
"track_seen": 1 | |||
} |
@@ -0,0 +1,162 @@ | |||
# 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.util import ( | |||
get_user_doc, | |||
check_url, | |||
dictify, | |||
get_emojis | |||
) | |||
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, 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.type = room.type | |||
mess.room = room.name | |||
mess.content = sanitize_message_content(content) | |||
mess.user = user.name | |||
mess.mentions = json.dumps(meta.mentions) | |||
mess.urls = ','.join(meta.urls) | |||
mess.save() | |||
if link: | |||
room.update(dict( | |||
last_message = mess.name | |||
)) | |||
room.save() | |||
return mess | |||
def get_new_chat_message(user, room, content): | |||
mess = get_new_chat_message_doc(user, room, content) | |||
resp = dict( | |||
name = mess.name, | |||
user = mess.user, | |||
room = mess.room, | |||
content = 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() | |||
def send(user, room, content): | |||
mess = get_new_chat_message(user, room, content) | |||
frappe.publish_realtime('frappe.chat.message:create', mess, room = room, | |||
after_commit = True) | |||
@frappe.whitelist() | |||
def seen(message, user = None): | |||
mess = frappe.get_doc('Chat Message', message) | |||
mess.add_seen(user) | |||
room = mess.room | |||
resp = dict(message = message, data = dict(seen = json.loads(mess._seen))) | |||
frappe.publish_realtime('frappe.chat.message:update', resp, room = room, after_commit = True) | |||
# This is fine for now. If you're "ReST"-ing it, | |||
# make sure you don't let the user see them. | |||
# Come again, Why are we even passing user? | |||
def get_messages(room, user = None, fields = None, pagination = 20): | |||
user = get_user_doc(user) | |||
room = frappe.get_doc('Chat Room', room) | |||
mess = frappe.get_list('Chat Message', | |||
filters = [ | |||
('Chat Message', 'room', '=', room.name), | |||
('Chat Message', 'type', '=', room.type) | |||
], | |||
fields = fields if fields else [ | |||
'name', 'type', | |||
'room', 'content', | |||
'user', 'mentions', 'urls', | |||
'creation' | |||
] | |||
) | |||
return mess |
@@ -0,0 +1,23 @@ | |||
/* eslint-disable */ | |||
// rename this file from _test_[name] to test_[name] to activate | |||
// and remove above this line | |||
QUnit.test("test: Chat Message", function (assert) { | |||
let done = assert.async(); | |||
// number of asserts | |||
assert.expect(1); | |||
frappe.run_serially([ | |||
// insert a new Chat Message | |||
() => frappe.tests.make('Chat Message', [ | |||
// values to be set | |||
{key: 'value'} | |||
]), | |||
() => { | |||
assert.equal(cur_frm.doc.key, 'value'); | |||
}, | |||
() => done() | |||
]); | |||
}); |
@@ -0,0 +1,19 @@ | |||
# imports - standard imports | |||
import unittest | |||
# imports - module imports | |||
import frappe | |||
# imports - frappe module imports | |||
from frappe.chat.doctype.chat_message import chat_message | |||
from frappe.chat.util import create_test_user | |||
session = frappe.session | |||
test_user = create_test_user(__name__) | |||
class TestChatMessage(unittest.TestCase): | |||
def test_send(self): | |||
# TODO - Write the case once you're done with Chat Room | |||
# user = test_user | |||
# chat_message.send(user, room, 'foobar') | |||
pass |
@@ -0,0 +1,71 @@ | |||
{ | |||
"allow_copy": 0, | |||
"allow_guest_to_view": 0, | |||
"allow_import": 0, | |||
"allow_rename": 0, | |||
"beta": 1, | |||
"creation": "2017-11-15 13:27:05.706207", | |||
"custom": 0, | |||
"docstatus": 0, | |||
"doctype": "DocType", | |||
"document_type": "", | |||
"editable_grid": 1, | |||
"engine": "InnoDB", | |||
"fields": [ | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "attachment", | |||
"fieldtype": "Attach", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 1, | |||
"in_standard_filter": 0, | |||
"label": "Attachment", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 1, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
} | |||
], | |||
"has_web_view": 0, | |||
"hide_heading": 0, | |||
"hide_toolbar": 0, | |||
"idx": 0, | |||
"image_view": 0, | |||
"in_create": 0, | |||
"is_submittable": 0, | |||
"issingle": 0, | |||
"istable": 1, | |||
"max_attachments": 0, | |||
"modified": "2017-11-15 13:33:27.405470", | |||
"modified_by": "Administrator", | |||
"module": "Chat", | |||
"name": "Chat Message Attachment", | |||
"name_case": "", | |||
"owner": "Administrator", | |||
"permissions": [], | |||
"quick_entry": 1, | |||
"read_only": 0, | |||
"read_only_onload": 0, | |||
"show_name_in_global_search": 0, | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"track_changes": 1, | |||
"track_seen": 0 | |||
} |
@@ -0,0 +1,10 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2017, Frappe Technologies and contributors | |||
# For license information, please see license.txt | |||
from __future__ import unicode_literals | |||
import frappe | |||
from frappe.model.document import Document | |||
class ChatMessageAttachment(Document): | |||
pass |
@@ -0,0 +1,8 @@ | |||
// Copyright (c) 2017, Frappe Technologies and contributors | |||
// For license information, please see license.txt | |||
frappe.ui.form.on('Chat Profile', { | |||
refresh: function(frm) { | |||
} | |||
}); |
@@ -0,0 +1,278 @@ | |||
{ | |||
"allow_copy": 0, | |||
"allow_guest_to_view": 0, | |||
"allow_import": 0, | |||
"allow_rename": 0, | |||
"autoname": "CP.#####", | |||
"beta": 1, | |||
"creation": "2017-11-13 18:26:57.943027", | |||
"custom": 0, | |||
"docstatus": 0, | |||
"doctype": "DocType", | |||
"document_type": "", | |||
"editable_grid": 1, | |||
"engine": "InnoDB", | |||
"fields": [ | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"default": "Online", | |||
"fieldname": "status", | |||
"fieldtype": "Select", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 1, | |||
"in_standard_filter": 0, | |||
"label": "Status", | |||
"length": 0, | |||
"no_copy": 0, | |||
"options": "Online\nAway\nBusy\nOffline", | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 1, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "chat_background", | |||
"fieldtype": "Attach Image", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Chat Background", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "notifications", | |||
"fieldtype": "Section Break", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Notifications", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"default": "1", | |||
"fieldname": "notification_tones", | |||
"fieldtype": "Check", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Notification Tones", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"default": "1", | |||
"fieldname": "conversation_tones", | |||
"fieldtype": "Check", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Conversation Tones", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "settings", | |||
"fieldtype": "Section Break", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Settings", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"default": "1", | |||
"fieldname": "display_widget", | |||
"fieldtype": "Check", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Display Widget", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
} | |||
], | |||
"has_web_view": 0, | |||
"hide_heading": 0, | |||
"hide_toolbar": 0, | |||
"idx": 0, | |||
"image_view": 0, | |||
"in_create": 0, | |||
"is_submittable": 0, | |||
"issingle": 0, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-12-19 11:23:04.791395", | |||
"modified_by": "achilles@erpnext.com", | |||
"module": "Chat", | |||
"name": "Chat Profile", | |||
"name_case": "", | |||
"owner": "Administrator", | |||
"permissions": [ | |||
{ | |||
"amend": 0, | |||
"apply_user_permissions": 0, | |||
"cancel": 0, | |||
"create": 1, | |||
"delete": 1, | |||
"email": 1, | |||
"export": 1, | |||
"if_owner": 0, | |||
"import": 0, | |||
"permlevel": 0, | |||
"print": 1, | |||
"read": 1, | |||
"report": 1, | |||
"role": "System Manager", | |||
"set_user_permissions": 0, | |||
"share": 1, | |||
"submit": 0, | |||
"write": 1 | |||
} | |||
], | |||
"quick_entry": 1, | |||
"read_only": 0, | |||
"read_only_onload": 0, | |||
"show_name_in_global_search": 0, | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"track_changes": 1, | |||
"track_seen": 0 | |||
} |
@@ -0,0 +1,173 @@ | |||
# imports - module imports | |||
from frappe.model.document import Document | |||
from frappe import _, _dict # <- the best thing ever happened to frappe | |||
import frappe | |||
# imports - frappe module imports | |||
from frappe.core.doctype.version.version import get_diff | |||
from frappe.chat.doctype.chat_room.chat_room import get_user_chat_rooms | |||
from frappe.chat.util import ( | |||
get_user_doc, | |||
safe_json_loads, | |||
filter_dict, | |||
dictify | |||
) | |||
session = frappe.session | |||
# TODO | |||
# User | |||
# [ ] Deleting a User should also delete its Chat Profile. | |||
# [ ] Ensuring username is mandatory when User has been created. | |||
# Chat Profile | |||
# [x] Link Chat Profile DocType to User when User has been created. | |||
# [x] Once done, add a validator to check Chat Profile has been | |||
# created only once. | |||
# [x] Users can view other Users Chat Profile, but not update the same. | |||
# Not sure, but circular link would be helpful. | |||
class ChatProfile(Document): | |||
# trigger from DocType | |||
def before_save(self): | |||
if not self.is_new(): | |||
self.get_doc_before_save() | |||
def on_update(self): | |||
user = get_user_doc() | |||
if user.chat_profile: | |||
if user.chat_profile != self.name: | |||
frappe.throw(_("Sorry! You don't have permission to update this profile.")) | |||
else: | |||
if not self.is_new(): | |||
before = self.get_doc_before_save() | |||
after = self | |||
diff = dictify(get_diff(before, after)) | |||
if diff: | |||
fields = [change[0] for change in diff.changed] | |||
# NOTE: Version DocType is the best thing ever. Selective Updates to Chat Rooms/Users FTW. | |||
# status update are dispatched to current user and Direct Chat Rooms. | |||
if 'status' in fields: | |||
# TODO: you can add filters within get_user_chat_rooms | |||
rooms = get_user_chat_rooms(user) | |||
rooms = [r for r in rooms if r.type == 'Direct'] | |||
resp = dict( | |||
user = user.name, | |||
data = dict( | |||
status = self.status | |||
) | |||
) | |||
for room in rooms: | |||
frappe.publish_realtime('frappe.chat.profile:update', resp, room = room.name, after_commit = True) | |||
if 'display_widget' in fields: | |||
resp = dict( | |||
user = user.name, | |||
data = dict( | |||
display_widget = bool(self.display_widget) | |||
) | |||
) | |||
frappe.publish_realtime('frappe.chat.profile:update', resp, user = user.name, after_commit = True) | |||
def get_user_chat_profile_doc(user = None): | |||
user = get_user_doc(user) | |||
prof = frappe.get_doc('Chat Profile', user.chat_profile) | |||
return prof | |||
def get_user_chat_profile(user = None, fields = None): | |||
''' | |||
Returns the Chat Profile for a given user. | |||
''' | |||
user = get_user_doc(user) | |||
prof = get_user_chat_profile_doc(user) | |||
data = dict( | |||
name = user.name, | |||
email = user.email, | |||
first_name = user.first_name, | |||
last_name = user.last_name, | |||
username = user.username, | |||
avatar = user.user_image, | |||
bio = user.bio, | |||
status = prof.status, | |||
chat_bg = prof.chat_background, | |||
notification_tones = bool(prof.notification_tones), | |||
conversation_tones = bool(prof.conversation_tones), # frappe, y u no jsonify 0,1 bools? :( | |||
display_widget = bool(prof.display_widget) | |||
) | |||
try: | |||
data = filter_dict(data, fields) | |||
except KeyError as e: | |||
frappe.throw(str(e)) | |||
return data | |||
def get_new_chat_profile_doc(user = None, link = True): | |||
user = get_user_doc(user) | |||
prof = frappe.new_doc('Chat Profile') | |||
prof.save() | |||
if link: | |||
user.update(dict( | |||
chat_profile = prof.name | |||
)) | |||
user.save() | |||
return prof | |||
@frappe.whitelist() | |||
def create(user, exists_ok = False, fields = None): | |||
''' | |||
Creates a Chat Profile for the current session user, throws error if exists. | |||
''' | |||
exists, fields = safe_json_loads(exists_ok, fields) | |||
user = get_user_doc(user) | |||
if user.name != session.user: | |||
frappe.throw(_("Sorry! You don't have permission to create a profile for user {name}.".format( | |||
name = user.name | |||
))) | |||
if user.chat_profile: | |||
if not exists: | |||
frappe.throw(_("Sorry! You cannot create more than one Chat Profile.")) | |||
prof = get_user_chat_profile(user, fields) | |||
else: | |||
prof = get_new_chat_profile_doc(user) | |||
prof = get_user_chat_profile(user, fields) | |||
return dictify(prof) | |||
@frappe.whitelist() | |||
def get(user = None, fields = None): | |||
''' | |||
Returns a user's Chat Profile. | |||
''' | |||
fields = safe_json_loads(fields) | |||
prof = get_user_chat_profile(user, fields) | |||
return dictify(prof) | |||
@frappe.whitelist() | |||
def update(user, data): | |||
data = safe_json_loads(data) | |||
user = get_user_doc(user) | |||
if user.name != session.user: | |||
frappe.throw(_("Sorry! You don't have permission to update Chat Profile for user {name}.".format( | |||
name = user.name | |||
))) | |||
prof = get_user_chat_profile_doc(user) | |||
prof.update(data) | |||
prof.save() |
@@ -0,0 +1,11 @@ | |||
frappe.listview_settings['Chat Profile'] = | |||
{ | |||
get_indicator: function (doc) | |||
{ | |||
const status = frappe._.squash(frappe.chat.profile.STATUSES.filter( | |||
s => s.name === doc.status | |||
)); | |||
return [__(status.name), status.color, `status,=,${status.name}`] | |||
} | |||
}; |
@@ -0,0 +1,23 @@ | |||
/* eslint-disable */ | |||
// rename this file from _test_[name] to test_[name] to activate | |||
// and remove above this line | |||
QUnit.test("test: Chat Profile", function (assert) { | |||
let done = assert.async(); | |||
// number of asserts | |||
assert.expect(1); | |||
frappe.run_serially([ | |||
// insert a new Chat Profile | |||
() => frappe.tests.make('Chat Profile', [ | |||
// values to be set | |||
{key: 'value'} | |||
]), | |||
() => { | |||
assert.equal(cur_frm.doc.key, 'value'); | |||
}, | |||
() => done() | |||
]); | |||
}); |
@@ -0,0 +1,59 @@ | |||
# imports - standard imports | |||
import unittest | |||
# imports - module imports | |||
import frappe | |||
# imports - frappe module imports | |||
from frappe.chat.doctype.chat_profile import chat_profile | |||
from frappe.chat.util import get_user_doc, create_test_user | |||
session = frappe.session | |||
test_user = create_test_user(__name__) | |||
class TestChatProfile(unittest.TestCase): | |||
def test_create(self): | |||
with self.assertRaises(frappe.ValidationError): | |||
chat_profile.create(test_user) | |||
user = get_user_doc(session.user) | |||
if not user.chat_profile: | |||
chat_profile.create(user.name) | |||
prof = chat_profile.get(user.name) | |||
self.assertEquals(prof.status, 'Online') | |||
else: | |||
with self.assertRaises(frappe.ValidationError): | |||
chat_profile.create(user.name) | |||
def test_get(self): | |||
user = session.user | |||
prof = chat_profile.get(user) | |||
self.assertNotEquals(len(prof), 1) | |||
prof = chat_profile.get(user, fields = ['status']) | |||
self.assertEquals(len(prof), 1) | |||
self.assertEquals(prof.status, 'Online') | |||
prof = chat_profile.get(user, fields = ['status', 'chat_bg']) | |||
self.assertEquals(len(prof), 2) | |||
def test_update(self): | |||
user = test_user | |||
with self.assertRaises(frappe.ValidationError): | |||
prof = chat_profile.update(user, data = dict( | |||
status = 'Online' | |||
)) | |||
user = get_user_doc(session.user) | |||
prev = chat_profile.get(user.name) | |||
chat_profile.update(user.name, data = dict( | |||
status = 'Offline' | |||
)) | |||
prof = chat_profile.get(user.name) | |||
self.assertEquals(prof.status, 'Offline') | |||
# revert | |||
chat_profile.update(user.name, data = dict( | |||
status = prev.status | |||
)) |
@@ -0,0 +1,8 @@ | |||
// Copyright (c) 2017, Frappe Technologies and contributors | |||
// For license information, please see license.txt | |||
frappe.ui.form.on('Chat Room', { | |||
refresh: function(frm) { | |||
} | |||
}); |
@@ -0,0 +1,314 @@ | |||
{ | |||
"allow_copy": 0, | |||
"allow_guest_to_view": 0, | |||
"allow_import": 0, | |||
"allow_rename": 0, | |||
"autoname": "CR.#####", | |||
"beta": 1, | |||
"creation": "2017-11-08 15:27:21.156667", | |||
"custom": 0, | |||
"docstatus": 0, | |||
"doctype": "DocType", | |||
"document_type": "", | |||
"editable_grid": 1, | |||
"engine": "InnoDB", | |||
"fields": [ | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"default": "Direct", | |||
"fieldname": "type", | |||
"fieldtype": "Select", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 1, | |||
"in_standard_filter": 0, | |||
"label": "Type", | |||
"length": 0, | |||
"no_copy": 0, | |||
"options": "Direct\nGroup\nVisitor", | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 1, | |||
"search_index": 0, | |||
"set_only_once": 1, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"depends_on": "eval:doc.type==\"Group\"", | |||
"fieldname": "room_name", | |||
"fieldtype": "Data", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Name", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"depends_on": "eval:doc.type==\"Group\"", | |||
"fieldname": "avatar", | |||
"fieldtype": "Attach Image", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Avatar", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "last_message", | |||
"fieldtype": "Data", | |||
"hidden": 1, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Last Message", | |||
"length": 0, | |||
"no_copy": 0, | |||
"options": "", | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "message_count", | |||
"fieldtype": "Int", | |||
"hidden": 1, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Message Count", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"default": "", | |||
"fieldname": "owner", | |||
"fieldtype": "Link", | |||
"hidden": 1, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Owner", | |||
"length": 0, | |||
"no_copy": 0, | |||
"options": "User", | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 1, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "user_list", | |||
"fieldtype": "Section Break", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Users", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "users", | |||
"fieldtype": "Table", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Users", | |||
"length": 0, | |||
"no_copy": 0, | |||
"options": "Chat Room User", | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
} | |||
], | |||
"has_web_view": 0, | |||
"hide_heading": 0, | |||
"hide_toolbar": 0, | |||
"idx": 0, | |||
"image_field": "avatar", | |||
"image_view": 0, | |||
"in_create": 0, | |||
"is_submittable": 0, | |||
"issingle": 0, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-12-17 15:53:24.103274", | |||
"modified_by": "achilles@erpnext.com", | |||
"module": "Chat", | |||
"name": "Chat Room", | |||
"name_case": "", | |||
"owner": "Administrator", | |||
"permissions": [ | |||
{ | |||
"amend": 0, | |||
"apply_user_permissions": 0, | |||
"cancel": 0, | |||
"create": 1, | |||
"delete": 1, | |||
"email": 1, | |||
"export": 1, | |||
"if_owner": 0, | |||
"import": 0, | |||
"permlevel": 0, | |||
"print": 1, | |||
"read": 1, | |||
"report": 1, | |||
"role": "System Manager", | |||
"set_user_permissions": 1, | |||
"share": 1, | |||
"submit": 0, | |||
"write": 1 | |||
} | |||
], | |||
"quick_entry": 0, | |||
"read_only": 0, | |||
"read_only_onload": 0, | |||
"search_fields": "room_name", | |||
"show_name_in_global_search": 1, | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"title_field": "room_name", | |||
"track_changes": 1, | |||
"track_seen": 0 | |||
} |
@@ -0,0 +1,296 @@ | |||
# imports - standard imports | |||
import json | |||
# imports - module imports | |||
import frappe | |||
from frappe.model.document import Document | |||
from frappe import _, _dict | |||
# imports - frappe module imports | |||
from frappe.core.doctype.version.version import get_diff | |||
from frappe.chat.doctype.chat_message.chat_message import get_messages | |||
from frappe.chat.util import ( | |||
get_user_doc, | |||
safe_json_loads, | |||
dictify, | |||
listify, | |||
squashify, | |||
assign_if_none | |||
) | |||
session = frappe.session | |||
# TODO | |||
# [x] Only Owners can edit the DocType Record. | |||
# [ ] Show Chat Room List that only has session.user in it. | |||
# [ ] Make Chat Room pagination configurable. | |||
class ChatRoom(Document): | |||
def validate(self): | |||
# TODO - Validations | |||
# [x] Direct/Visitor must have 2 users only. | |||
# [x] Groups must have 1 (1 being ther session user) or more users. | |||
# [x] Ensure group has a name. | |||
# [x] Check if there is a one-to-one conversation between 2 users (if Direct/Visitor). | |||
# First, check if user isn't stupid by adding many users or himself/herself. | |||
# C'mon yo, you're the owner. | |||
users = get_chat_room_user_set(self.users) | |||
users = [u for u in users if u.user != session.user] | |||
self.update(dict( | |||
users = users | |||
)) | |||
if self.type in ("Direct", "Visitor"): | |||
if len(users) != 1: | |||
frappe.throw(_('{type} room must have atmost one user.'.format( | |||
type = self.type | |||
))) | |||
# squash to a single object if it's a list. | |||
other = squashify(users) | |||
# I don't know which idiot would make username not unique. | |||
# Remember, this entire app assumes validation based on user's email. | |||
# Okay, this must go only during creation. But alas, on click "Save" it does the same. | |||
if self.is_new(): | |||
if is_one_on_one(self.owner, other.user, bidirectional = True): | |||
frappe.throw(_('Direct room with {other} already exists.'.format( | |||
other = other.user | |||
))) | |||
if self.type == "Group" and not self.room_name: | |||
frappe.throw(_('Group name cannot be empty.')) | |||
# trigger from DocType | |||
def before_save(self): | |||
if not self.is_new(): | |||
self.get_doc_before_save() | |||
def on_update(self): | |||
user = session.user | |||
if user != self.owner: | |||
frappe.throw(_("Sorry! You don't enough permissions to update this room.")) | |||
if not self.is_new(): | |||
before = self.get_doc_before_save() | |||
after = self | |||
# TODO | |||
# [ ] Check if DocType is itself updated. WARN if not. | |||
diff = dictify(get_diff(before, after)) # whoever you are, thank you for this. | |||
if diff: | |||
# notify only if there is an update. | |||
update = dict() # Update Goodies. | |||
# Types of Differences | |||
# 1. Changes | |||
for changed in diff.changed: | |||
field, old, new = changed | |||
if field == 'last_message': | |||
doc_message = frappe.get_doc('Chat Message', new) | |||
update.update({ | |||
field: dict( | |||
name = doc_message.name, | |||
user = doc_message.user, | |||
room = doc_message.room, | |||
content = doc_message.content, | |||
urls = doc_message.urls, | |||
mentions = doc_message.mentions, | |||
creation = doc_message.creation | |||
) | |||
}) | |||
else: | |||
update.update({ | |||
field: new | |||
}) | |||
# 2. Added or Removed | |||
# TODO | |||
# [ ] Handle users. | |||
# I personally feel this could be done better by either creating a new event | |||
# or attaching to the below event. Questions like Who removed Whom? Who added Whom? etc. | |||
# For first-cut, let's simply update the latest user list. | |||
# This is because the Version DocType already handles it. | |||
if diff.added or diff.removed: | |||
update.update({ | |||
'users': [u.user for u in self.users] | |||
}) | |||
resp = dict( | |||
room = self.name, | |||
data = update | |||
) | |||
frappe.publish_realtime('frappe.chat.room:update', resp, | |||
room = self.name, after_commit = True) | |||
def on_trash(self): | |||
if self.owner != session.user: | |||
frappe.throw(_("Sorry, you're not authorized to delete this room.")) | |||
def is_admin(user, room): | |||
if user != session.user: | |||
frappe.throw(_("Sorry, you're not authorized to view this information.")) | |||
# TODO - I'm tired searching the bug. | |||
def is_one_on_one(owner, other, bidirectional = False): | |||
''' | |||
checks if the owner and other have a direct conversation room. | |||
''' | |||
def get_room(owner, other): | |||
room = frappe.get_list('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): | |||
''' | |||
Returns a set of Chat Room Users | |||
:param users: sequence of Chat Room Users | |||
:return: set of Chat Room Users | |||
''' | |||
seen, news = set(), list() | |||
for u in users: | |||
if u.user not in seen: | |||
news.append(u) | |||
seen.add(u.user) | |||
return news | |||
def get_new_chat_room_doc(kind, owner, users = None, name = None): | |||
room = frappe.new_doc('Chat Room') | |||
room.type = kind | |||
room.owner = owner | |||
room.room_name = name | |||
users = users if isinstance(users, list) or users is None else [users] | |||
docs = [ ] | |||
if users: | |||
for user in users: | |||
doc = frappe.new_doc('Chat Room User') | |||
doc.user = user | |||
docs.append(doc) | |||
room.users = docs | |||
room.save() | |||
return room | |||
def get_new_chat_room(kind, owner, users = None, name = None): | |||
room = get_new_chat_room_doc(kind = kind, owner = owner, users = users, name = name) | |||
room = get_user_chat_rooms(user = owner, rooms = room.name) | |||
return room | |||
def get_user_chat_rooms(user = None, rooms = None, fields = None): | |||
''' | |||
if user is None, defaults to session user. | |||
if room is None, returns the entire list of rooms subscribed by user. | |||
''' | |||
user = get_user_doc(user) | |||
rooms = assign_if_none(rooms, [ ]) | |||
fields = assign_if_none(fields, [ ]) | |||
param = [f for f in fields if f != 'users' or f != 'last_message'] | |||
rooms = frappe.get_list('Chat Room', | |||
or_filters = [ | |||
['Chat Room', 'owner', '=', user.name], | |||
['Chat Room User', 'user', '=', user.name] | |||
], | |||
filters = [ | |||
['Chat Room', 'name', 'in', rooms] | |||
] if rooms else None, | |||
fields = param + ['name'] if param or 'users' in fields else [ | |||
'type', 'name', 'owner', 'room_name', 'avatar', 'creation' | |||
], | |||
distinct = True | |||
) | |||
if not fields or 'users' in fields: | |||
for i, r in enumerate(rooms): | |||
doc_room = frappe.get_doc('Chat Room', r.name) | |||
rooms[i]['users'] = [ ] | |||
for user in doc_room.users: | |||
rooms[i]['users'].append(user.user) | |||
if not fields or 'last_message' in fields: | |||
for i, r in enumerate(rooms): | |||
doc_room = frappe.get_doc('Chat Room', r.name) | |||
if doc_room.last_message: | |||
doc_message = frappe.get_doc('Chat Message', doc_room.last_message) | |||
rooms[i]['last_message'] = dict( | |||
name = doc_message.name, | |||
user = doc_message.user, | |||
room = doc_message.room, | |||
content = doc_message.content, | |||
urls = doc_message.urls, | |||
mentions = doc_message.mentions, | |||
creation = doc_message.creation | |||
) | |||
else: | |||
rooms[i]['last_message'] = None | |||
rooms = dictify(rooms) | |||
return rooms | |||
@frappe.whitelist() | |||
def create(kind, owner, users = None, name = None): | |||
users = safe_json_loads(users) | |||
if owner != session.user: | |||
frappe.throw(_("Sorry! You're not authorized to create a Chat Room.")) | |||
room = get_new_chat_room(kind = kind, owner = owner, users = users, name = name) | |||
room = squashify(room) | |||
users = [room.owner] + [u for u in room.users] | |||
for u in users: | |||
frappe.publish_realtime('frappe.chat.room:create', room, | |||
user = u, after_commit = True) | |||
return room | |||
@frappe.whitelist() | |||
def get(user, rooms = None, fields = None): | |||
rooms = safe_json_loads(rooms) | |||
fields = safe_json_loads(fields) | |||
user = get_user_doc(user) | |||
if user.name != frappe.session.user: | |||
frappe.throw(_("You're not authorized to view this room.")) | |||
data = get_user_chat_rooms(user = user, rooms = rooms, fields = fields) | |||
return data | |||
# Could we move pagination to a config, but how? | |||
# One possibility is to add to Chat Profile itself. | |||
# Actually yes. | |||
@frappe.whitelist() | |||
def get_history(room, user = None, pagination = 20): | |||
user = get_user_doc(user) | |||
mess = get_messages(room, pagination = pagination) | |||
mess = squashify(mess) | |||
return dictify(mess) |
@@ -0,0 +1,23 @@ | |||
/* eslint-disable */ | |||
// rename this file from _test_[name] to test_[name] to activate | |||
// and remove above this line | |||
QUnit.test("test: Chat Room", function (assert) { | |||
let done = assert.async(); | |||
// number of asserts | |||
assert.expect(1); | |||
frappe.run_serially([ | |||
// insert a new Chat Room | |||
() => frappe.tests.make('Chat Room', [ | |||
// values to be set | |||
{key: 'value'} | |||
]), | |||
() => { | |||
assert.equal(cur_frm.doc.key, 'value'); | |||
}, | |||
() => done() | |||
]); | |||
}); |
@@ -0,0 +1,10 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright (c) 2017, Frappe Technologies and Contributors | |||
# See license.txt | |||
from __future__ import unicode_literals | |||
import frappe | |||
import unittest | |||
class TestChatRoom(unittest.TestCase): | |||
pass |
@@ -0,0 +1,102 @@ | |||
{ | |||
"allow_copy": 0, | |||
"allow_guest_to_view": 0, | |||
"allow_import": 0, | |||
"allow_rename": 0, | |||
"beta": 1, | |||
"creation": "2017-11-08 15:24:21.029314", | |||
"custom": 0, | |||
"docstatus": 0, | |||
"doctype": "DocType", | |||
"document_type": "", | |||
"editable_grid": 1, | |||
"engine": "InnoDB", | |||
"fields": [ | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "user", | |||
"fieldtype": "Link", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 1, | |||
"in_standard_filter": 0, | |||
"label": "User", | |||
"length": 0, | |||
"no_copy": 0, | |||
"options": "User", | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "is_admin", | |||
"fieldtype": "Check", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Admin", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
} | |||
], | |||
"has_web_view": 0, | |||
"hide_heading": 0, | |||
"hide_toolbar": 0, | |||
"idx": 0, | |||
"image_view": 0, | |||
"in_create": 0, | |||
"is_submittable": 0, | |||
"issingle": 0, | |||
"istable": 1, | |||
"max_attachments": 0, | |||
"modified": "2017-11-28 11:50:06.165435", | |||
"modified_by": "achilles@erpnext.com", | |||
"module": "Chat", | |||
"name": "Chat Room User", | |||
"name_case": "", | |||
"owner": "Administrator", | |||
"permissions": [], | |||
"quick_entry": 1, | |||
"read_only": 0, | |||
"read_only_onload": 0, | |||
"show_name_in_global_search": 0, | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"track_changes": 1, | |||
"track_seen": 0 | |||
} |
@@ -0,0 +1,8 @@ | |||
# imports - module imports | |||
from frappe.model.document import Document | |||
import frappe | |||
session = frappe.session | |||
class ChatRoomUser(Document): | |||
pass |
@@ -0,0 +1,11 @@ | |||
frappe.pages.chat.on_page_load = function (container) | |||
{ | |||
const page = new frappe.ui.Page({ | |||
title: __('Chat'), parent: container | |||
}); | |||
const $container = $(container).find('.layout-main') | |||
$container.html("") | |||
// const chat = new frappe.Chat($container, { layout: frappe.Chat.Layout.PAGE }); | |||
// chat.render(); | |||
}; |
@@ -0,0 +1,20 @@ | |||
{ | |||
"content": null, | |||
"creation": "2017-11-08 14:55:47.986307", | |||
"docstatus": 0, | |||
"doctype": "Page", | |||
"icon": "octicon octiocn-comment", | |||
"idx": 0, | |||
"modified": "2017-12-17 10:44:23.698446", | |||
"modified_by": "Administrator", | |||
"module": "Chat", | |||
"name": "chat", | |||
"owner": "Administrator", | |||
"page_name": "chat", | |||
"roles": [], | |||
"script": null, | |||
"standard": "Yes", | |||
"style": null, | |||
"system_page": 0, | |||
"title": "Chat" | |||
} |
@@ -0,0 +1,27 @@ | |||
QUnit.test("test: Chat", function (assert) | |||
{ | |||
const done = assert.async(3); | |||
assert.expect(3); | |||
// test - frappe._.fuzzy_search | |||
frappe.run_serially([ | |||
() => assert.equal(frappe._.fuzzy_search("foo", ["foobar", "tooti"]), "foobar"), | |||
]); | |||
// test - frappe.chat.profile.create | |||
frappe.run_serially([ | |||
() => frappe.set_route('chat'), | |||
// empty promise | |||
() => frappe.chat.profile.create(), | |||
(profile) => { | |||
assert.equal(profile.status, "Online"); | |||
}, | |||
// one key only | |||
() => frappe.chat.profile.create("status"), | |||
(profile) => { | |||
assert.equal(Object.keys(profile).length, 1); | |||
}, | |||
() => done() | |||
]); | |||
}); |
@@ -0,0 +1,13 @@ | |||
# imports - module imports | |||
from frappe.chat.util.util import ( | |||
get_user_doc, | |||
squashify, | |||
safe_json_loads, | |||
filter_dict, | |||
assign_if_none, | |||
listify, | |||
dictify, | |||
check_url, | |||
create_test_user, | |||
get_emojis | |||
) |
@@ -0,0 +1,35 @@ | |||
# 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.assertEquals(type(number), int) | |||
number = safe_json_loads("1.0") | |||
self.assertEquals(type(number), float) | |||
string = safe_json_loads("foobar") | |||
self.assertEquals(type(string), str) | |||
array = safe_json_loads('[{ "foo": "bar" }]') | |||
self.assertEquals(type(array), list) | |||
objekt = safe_json_loads('{ "foo": "bar" }') | |||
self.assertEquals(type(objekt), dict) | |||
true, null = safe_json_loads("true", "null") | |||
self.assertEquals(true, True) | |||
self.assertEquals(null, None) | |||
def test_get_user_doc(self): | |||
# Needs more test cases. | |||
user = get_user_doc() | |||
self.assertEquals(user.name, frappe.session.user) |
@@ -0,0 +1,114 @@ | |||
# imports - third-party imports | |||
import requests | |||
# imports - compatibility imports | |||
import six | |||
# imports - standard imports | |||
from collections import MutableSequence, Mapping, MutableMapping | |||
if six.PY2: | |||
from urlparse import urlparse # PY2 | |||
else: | |||
from urllib.parse import urlparse # PY3 | |||
import json | |||
# imports - module imports | |||
from frappe.model.document import Document | |||
from frappe.exceptions import DuplicateEntryError | |||
from frappe import _dict | |||
import frappe | |||
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, MutableSequence) 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 as e: | |||
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 assign_if_none(a, b): | |||
if a is None: | |||
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 = _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) |
@@ -306,6 +306,7 @@ def console(context): | |||
@click.option('--test', multiple=True, help="Specific test") | |||
@click.option('--driver', help="For Travis") | |||
@click.option('--ui-tests', is_flag=True, default=False, help="Run UI Tests") | |||
@click.option('--coverage', is_flag=True, default=False, help='Display Coverage Report') | |||
@click.option('--module', help="Run tests in a module") | |||
@click.option('--profile', is_flag=True, default=False) | |||
@click.option('--junit-xml-output', help="Destination file path for junit xml report") | |||
@@ -273,7 +273,7 @@ | |||
"no_copy": 0, | |||
"oldfieldname": "standard", | |||
"oldfieldtype": "Select", | |||
"options": "\nYes\nNo", | |||
"options": "Yes\nNo", | |||
"permlevel": 0, | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
@@ -358,7 +358,7 @@ | |||
"issingle": 0, | |||
"istable": 0, | |||
"max_attachments": 0, | |||
"modified": "2017-05-03 17:24:10.162110", | |||
"modified": "2017-11-13 16:37:04.422547", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "Page", | |||
@@ -0,0 +1,23 @@ | |||
/* eslint-disable */ | |||
// rename this file from _test_[name] to test_[name] to activate | |||
// and remove above this line | |||
QUnit.test("test: Page", function (assert) { | |||
let done = assert.async(); | |||
// number of asserts | |||
assert.expect(1); | |||
frappe.run_serially([ | |||
// insert a new Page | |||
() => frappe.tests.make('Page', [ | |||
// values to be set | |||
{key: 'value'} | |||
]), | |||
() => { | |||
assert.equal(cur_frm.doc.key, 'value'); | |||
}, | |||
() => done() | |||
]); | |||
}); |
@@ -2048,6 +2048,67 @@ | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "chat_section_break", | |||
"fieldtype": "Section Break", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Chat", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "chat_profile", | |||
"fieldtype": "Link", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Chat Profile", | |||
"length": 0, | |||
"no_copy": 0, | |||
"options": "Chat Profile", | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 1, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
} | |||
], | |||
"has_web_view": 0, | |||
@@ -2063,8 +2124,8 @@ | |||
"istable": 0, | |||
"max_attachments": 5, | |||
"menu_index": 0, | |||
"modified": "2017-11-01 09:04:51.151347", | |||
"modified_by": "manas@erpnext.com", | |||
"modified": "2017-11-15 13:01:00.085916", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "User", | |||
"owner": "Administrator", | |||
@@ -16,6 +16,9 @@ from frappe.limits import get_limits | |||
from frappe.website.utils import is_signup_enabled | |||
from frappe.utils.background_jobs import enqueue | |||
# imports - frappe.chat | |||
from frappe.chat.doctype.chat_profile.chat_profile import get_user_chat_profile_doc | |||
STANDARD_USERS = ("Guest", "Administrator") | |||
class MaxUsersReachedError(frappe.ValidationError): pass | |||
@@ -1,42 +0,0 @@ | |||
.message-row .bot-icon { | |||
width: 24px; | |||
margin-right: 5px; | |||
padding-left: 7px; | |||
font-size: 18px; | |||
/*color: #FEEF72;*/ | |||
} | |||
.message-bubble { | |||
padding: 0px 10px; | |||
padding-top: 2px; | |||
border-radius: 10px; | |||
margin-bottom: 10px; | |||
background-color: #d1d8dd; | |||
width: 70%; | |||
} | |||
.message-bubble::before { | |||
background-color: #d1d8dd; | |||
content: "\00a0"; | |||
display: block; | |||
height: 6px; | |||
position: absolute; | |||
top: 7px; | |||
left: 70px; | |||
transform: rotate( 45deg ) skew( -35deg ); | |||
-moz-transform: rotate( 45deg ) skew( -35deg ); | |||
-ms-transform: rotate( 45deg ) skew( -35deg ); | |||
-o-transform: rotate( 45deg ) skew( -35deg ); | |||
-webkit-transform: rotate( 45deg ) skew( -35deg ); | |||
width: 14px; | |||
} | |||
.message-bubble.my-message { | |||
color: white; | |||
background-color: #5E64FF; | |||
} | |||
.message-bubble.my-message::before { | |||
background-color: #5E64FF; | |||
} |
@@ -1,239 +0,0 @@ | |||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
// MIT License. See license.txt | |||
frappe.pages.chat.on_page_load = function(parent) { | |||
var page = frappe.ui.make_app_page({ | |||
parent: parent, | |||
}); | |||
page.set_title('<span class="hidden-xs">' + __("Chat") + '</span>' | |||
+ '<span class="hidden-sm hidden-md hidden-lg message-to"></span>'); | |||
$(".navbar-center").html(__("Chat")); | |||
frappe.pages.chat.chat = new frappe.Chat(parent); | |||
} | |||
frappe.pages.chat.on_page_show = function() { | |||
// clear title prefix | |||
frappe.utils.set_title_prefix(""); | |||
frappe.breadcrumbs.add("Desk"); | |||
} | |||
frappe.Chat = Class.extend({ | |||
init: function(wrapper, page) { | |||
this.wrapper = wrapper; | |||
this.page = wrapper.page; | |||
this.make(); | |||
this.page.sidebar.addClass("col-sm-3"); | |||
this.page.wrapper.find(".layout-main-section-wrapper").addClass("col-sm-9"); | |||
this.page.wrapper.find(".page-title").removeClass("col-xs-6").addClass("col-xs-12"); | |||
this.page.wrapper.find(".page-actions").removeClass("col-xs-6").addClass("hidden-xs"); | |||
this.setup_realtime(); | |||
}, | |||
make: function() { | |||
this.make_sidebar(); | |||
}, | |||
setup_realtime: function() { | |||
var me = this; | |||
frappe.realtime.on('new_message', function(comment) { | |||
if(comment.modified_by !== frappe.session.user || comment.communication_type === 'Bot') { | |||
if(frappe.get_route()[0] === 'chat') { | |||
var current_contact = $(cur_page.page).find('[data-contact]').data('contact'); | |||
var on_broadcast_page = current_contact === frappe.session.user; | |||
if ((current_contact == comment.owner) | |||
|| (on_broadcast_page && comment.broadcast) | |||
|| current_contact === 'Bot' && comment.communication_type === 'Bot') { | |||
setTimeout(function() { me.prepend_comment(comment); }, 1000); | |||
} | |||
} else { | |||
frappe.utils.notify(__("Message from {0}", [frappe.user_info(comment.owner).fullname]), comment.content); | |||
} | |||
} | |||
}); | |||
}, | |||
prepend_comment: function(comment) { | |||
frappe.pages.chat.chat.list.data.unshift(comment); | |||
this.render_row(comment, true); | |||
}, | |||
make_sidebar: function() { | |||
var me = this; | |||
return frappe.call({ | |||
module:'frappe.desk', | |||
page:'chat', | |||
method:'get_active_users', | |||
callback: function(r,rt) { | |||
// sort | |||
r.message.sort(function(a, b) { return cint(b.has_session) - cint(a.has_session); }); | |||
// render | |||
me.page.sidebar.html(frappe.render_template("chat_sidebar", {data: r.message})); | |||
// bind click | |||
me.page.sidebar.find("a").on("click", function() { | |||
var li = $(this).parents("li:first"); | |||
if (li.hasClass("active")) | |||
return false; | |||
var contact = li.attr("data-user"); | |||
// active | |||
me.page.sidebar.find("li.active").removeClass("active"); | |||
me.page.sidebar.find('[data-user="'+ contact +'"]').addClass("active"); | |||
me.make_messages(contact); | |||
}); | |||
$(me.page.sidebar.find("a")[0]).click(); | |||
} | |||
}); | |||
}, | |||
make_messages: function(contact) { | |||
var me = this; | |||
this.page.main.html($(frappe.render_template("chat_main", { "contact": contact }))); | |||
var text_area = this.page.main.find(".messages-textarea").on("focusout", function() { | |||
// on touchscreen devices, scroll to top | |||
// so that static navbar and page head don't overlap the textarea | |||
if (frappe.dom.is_touchscreen()) { | |||
frappe.utils.scroll_to($(this).parents(".message-box")); | |||
} | |||
}); | |||
var post_btn = this.page.main.find(".btn-post").on("click", function() { | |||
var btn = $(this); | |||
var message_box = btn.parents(".message-box"); | |||
var textarea = message_box.find("textarea"); | |||
var contact = btn.attr("data-contact"); | |||
var txt = textarea.val(); | |||
var send_email = message_box.find('input[type="checkbox"]:checked').length > 0; | |||
if(txt) { | |||
return frappe.call({ | |||
module: 'frappe.desk', | |||
page:'chat', | |||
method:'post', | |||
args: { | |||
txt: txt, | |||
contact: contact, | |||
notify: send_email ? 1 : 0 | |||
}, | |||
callback:function(r,rt) { | |||
textarea.val(''); | |||
if (!r.exc) { | |||
me.prepend_comment(r.message); | |||
} | |||
}, | |||
btn: this | |||
}); | |||
} | |||
}); | |||
text_area.keydown("meta+return ctrl+return", function(e) { | |||
post_btn.trigger("click"); | |||
}); | |||
this.page.wrapper.find(".page-head .message-to").html(frappe.user.full_name(contact)); | |||
this.make_message_list(contact); | |||
this.list.run(); | |||
frappe.utils.scroll_to(); | |||
}, | |||
make_message_list: function(contact) { | |||
var me = this; | |||
this.list = new frappe.ui.BaseList({ | |||
parent: this.page.main.find(".message-list"), | |||
page: this.page, | |||
method: 'frappe.desk.page.chat.chat.get_list', | |||
args: { | |||
contact: contact | |||
}, | |||
hide_refresh: true, | |||
freeze: false, | |||
render_view: function (values) { | |||
values.map(function (value) { | |||
me.render_row(value); | |||
}); | |||
}, | |||
}); | |||
}, | |||
render_row: function(value, prepend) { | |||
this.prepare(value) | |||
var wrapper = $('<div class="list-row">') | |||
.data("data", this.meta) | |||
if(!prepend) | |||
wrapper.appendTo($(".result-list")).get(0); | |||
else | |||
wrapper.prependTo($(".result-list")).get(0); | |||
var row = $(frappe.render_template("chat_row", { | |||
data: value | |||
})).appendTo(wrapper) | |||
row.find(".avatar, .indicator").tooltip(); | |||
}, | |||
delete: function(ele) { | |||
$(ele).parent().css('opacity', 0.6); | |||
return frappe.call({ | |||
method: 'frappe.desk.page.chat.chat.delete', | |||
args: {name : $(ele).attr('data-name')}, | |||
callback: function() { | |||
$(ele).parents(".list-row:first").toggle(false); | |||
} | |||
}); | |||
}, | |||
refresh: function() {}, | |||
get_contact: function() { | |||
var route = location.hash; | |||
if(route.indexOf('/')!=-1) { | |||
var name = decodeURIComponent(route.split('/')[1]); | |||
if(name.indexOf('__at__')!=-1) { | |||
name = name.replace('__at__', '@'); | |||
} | |||
return name; | |||
} | |||
}, | |||
prepare: function(data) { | |||
if(data.communication_type==="Notification" || data.comment_type==="Shared") { | |||
data.is_system_message = 1; | |||
} | |||
if(data.owner==data.reference_name | |||
&& data.communication_type!=="Notification" | |||
&& data.comment_type!=="Bot") { | |||
data.is_public = true; | |||
} | |||
if(data.owner==data.reference_name && data.communication_type !== "Bot") { | |||
data.is_mine = true; | |||
} | |||
if(data.owner==data.reference_name && data.communication_type === "Bot") { | |||
data.owner = 'bot'; | |||
} | |||
data.content = frappe.markdown(data.content.substr(0, 1000)); | |||
} | |||
}); |
@@ -1,23 +0,0 @@ | |||
{ | |||
"content": null, | |||
"creation": "2012-06-14 18:44:56", | |||
"docstatus": 0, | |||
"doctype": "Page", | |||
"icon": "", | |||
"idx": 1, | |||
"modified": "2016-03-31 02:02:13.503910", | |||
"modified_by": "Administrator", | |||
"module": "Desk", | |||
"name": "chat", | |||
"owner": "Administrator", | |||
"page_name": "chat", | |||
"roles": [ | |||
{ | |||
"role": "All" | |||
} | |||
], | |||
"script": null, | |||
"standard": "Yes", | |||
"style": null, | |||
"title": "Chat" | |||
} |
@@ -1,145 +0,0 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# MIT License. See license.txt | |||
from __future__ import unicode_literals | |||
import frappe | |||
from frappe.desk.notifications import delete_notification_count_for | |||
from frappe.core.doctype.user.user import STANDARD_USERS | |||
from frappe.utils import cint | |||
from frappe import _ | |||
@frappe.whitelist() | |||
def get_list(arg=None): | |||
"""get list of messages""" | |||
frappe.form_dict['start'] = int(frappe.form_dict['start']) | |||
frappe.form_dict['page_length'] = int(frappe.form_dict['page_length']) | |||
frappe.form_dict['user'] = frappe.session['user'] | |||
# set all messages as read | |||
frappe.db.sql("""UPDATE `tabCommunication` set seen = 1 | |||
where | |||
communication_type in ('Chat', 'Notification') | |||
and seen = 0 | |||
and reference_doctype = 'User' | |||
and reference_name = %s""", frappe.session.user) | |||
delete_notification_count_for("Chat") | |||
frappe.local.flags.commit = True | |||
fields = '''name, owner, modified, content, communication_type, | |||
comment_type, reference_doctype, reference_name''' | |||
if frappe.form_dict.contact == 'Bot': | |||
return frappe.db.sql("""select {0} from `tabCommunication` | |||
where | |||
comment_type = 'Bot' | |||
and reference_doctype = 'User' | |||
and reference_name = %(user)s | |||
order by creation desc | |||
limit %(start)s, %(page_length)s""".format(fields), | |||
frappe.local.form_dict, as_dict=1) | |||
if frappe.form_dict.contact == frappe.session.user: | |||
# return messages | |||
return frappe.db.sql("""select {0} from `tabCommunication` | |||
where | |||
communication_type in ('Chat', 'Notification') | |||
and comment_type != 'Bot' | |||
and reference_doctype ='User' | |||
and (owner=%(contact)s | |||
or reference_name=%(user)s | |||
or owner=reference_name) | |||
order by creation desc | |||
limit %(start)s, %(page_length)s""".format(fields), | |||
frappe.local.form_dict, as_dict=1) | |||
else: | |||
return frappe.db.sql("""select {0} from `tabCommunication` | |||
where | |||
communication_type in ('Chat', 'Notification') | |||
and comment_type != 'Bot' | |||
and reference_doctype ='User' | |||
and ((owner=%(contact)s and reference_name=%(user)s) | |||
or (owner=%(user)s and reference_name=%(contact)s)) | |||
order by creation desc | |||
limit %(start)s, %(page_length)s""".format(fields), | |||
frappe.local.form_dict, as_dict=1) | |||
@frappe.whitelist() | |||
def get_active_users(): | |||
data = frappe.db.sql("""select name, | |||
(select count(*) from tabSessions where user=tabUser.name | |||
and timediff(now(), lastupdate) < time("01:00:00")) as has_session | |||
from tabUser | |||
where enabled=1 and | |||
ifnull(user_type, '')!='Website User' and | |||
name not in ({}) | |||
order by first_name""".format(", ".join(["%s"]*len(STANDARD_USERS))), STANDARD_USERS, as_dict=1) | |||
# make sure current user is at the top, using has_session = 100 | |||
users = [d.name for d in data] | |||
if frappe.session.user in users: | |||
data[users.index(frappe.session.user)]["has_session"] = 100 | |||
else: | |||
# in case of administrator | |||
data.append({"name": frappe.session.user, "has_session": 100}) | |||
if 'System Manager' in frappe.get_roles(): | |||
data.append({"name": "Bot", "has_session": 100}) | |||
return data | |||
@frappe.whitelist() | |||
def post(txt, contact, parenttype=None, notify=False, subject=None): | |||
"""post message""" | |||
comment_type = None | |||
if contact == 'Bot': | |||
contact = frappe.session.user | |||
comment_type = 'Bot' | |||
d = frappe.new_doc('Communication') | |||
d.communication_type = 'Notification' if parenttype else 'Chat' | |||
d.subject = subject | |||
d.content = txt | |||
d.reference_doctype = 'User' | |||
d.reference_name = contact | |||
d.sender = frappe.session.user | |||
if comment_type: | |||
d.comment_type = comment_type | |||
d.insert(ignore_permissions=True) | |||
delete_notification_count_for("Chat") | |||
if notify and cint(notify): | |||
_notify(contact, txt, subject) | |||
return d | |||
@frappe.whitelist() | |||
def delete(arg=None): | |||
frappe.get_doc("Communication", frappe.form_dict['name']).delete() | |||
def _notify(contact, txt, subject=None): | |||
from frappe.utils import get_fullname, get_url | |||
try: | |||
if not isinstance(contact, list): | |||
contact = [frappe.db.get_value("User", contact, "email") or contact] | |||
frappe.sendmail(\ | |||
recipients=contact, | |||
sender= frappe.db.get_value("User", frappe.session.user, "email"), | |||
subject=subject or _("New Message from {0}").format(get_fullname(frappe.session.user)), | |||
template="new_message", | |||
args={ | |||
"from": get_fullname(frappe.session.user), | |||
"message": txt, | |||
"link": get_url() | |||
}, | |||
header=[_('New Message'), 'orange']) | |||
except frappe.OutgoingEmailError: | |||
pass |
@@ -1,35 +0,0 @@ | |||
<div class="message-box"> | |||
<div class="media timeline-head"> | |||
<span class="pull-left avatar avatar-medium hidden-xs"> | |||
<img class="media-object" src="{%= frappe.user.image() %}"> | |||
</span> | |||
<div class="media-body"> | |||
<textarea style="height: 120px" style="margin-top: 10px;" | |||
class="form-control messages-textarea"></textarea> | |||
</div> | |||
<div style="padding-top: 15px;"> | |||
<span class="text-muted small hidden-xs" | |||
style="margin-left: 45px;">{{ __("Ctrl + Enter to post") }}</span> | |||
<button class="pull-right btn btn-primary btn-sm btn-post" data-contact="{%= contact %}"> | |||
{%= __("Post") %} | |||
</button> | |||
{% if (contact === user) { %} | |||
<span class="pull-right" | |||
style="margin-top: 4px; margin-right: 10px;"> | |||
<i class="octicon octicon-rss"></i> | |||
<span class="text-muted small">{%= __("Public") %}</span> | |||
</span> | |||
{% } %} | |||
<div class="pull-right checkbox text-muted small" | |||
style="margin-right: 15px; margin-top: 7px;"> | |||
<label> | |||
<input type="checkbox" class="is-email" | |||
style="margin-top: 1px"> | |||
{%= __("Email") %} | |||
</label> | |||
</div> | |||
<div class="clearfix"></div> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="message-list"></div> |
@@ -1,36 +0,0 @@ | |||
<div class="row message-row small"> | |||
<div class="col-sm-9"> | |||
<div class="media"> | |||
{% if (data.is_public) { %} | |||
<span class="pull-left hidden-xs" title="{{ __("Public") }}"> | |||
<i class="octicon octicon-rss text-muted" style="margin-top: 3px;"></i></span> | |||
{% } else { %} | |||
<span class="pull-left hidden-xs" | |||
style="width: 20px; height: 16px; display: inline-block;"></span> | |||
{% } %} | |||
<div class="pull-left hidden-xs"> | |||
<span class="avatar avatar-small" title="{%= frappe.user.full_name(data.owner) %} "> | |||
<img class="media-object {{ data.is_system_message ? "grayscale" : "" }}" | |||
src="{%= frappe.user.image(data.owner) %}"> | |||
</span> | |||
</div> | |||
<div class="media-body {{ data.is_system_message ? "system-message" : "" }} message-bubble {{ data.is_mine ? "my-message" : "" }}"> | |||
{%= data.content %} | |||
</div> | |||
</div> | |||
</div> | |||
<div class="col-sm-3 text-right message-row-right"> | |||
<div class="text-muted"> | |||
<span class="hidden-sm hidden-md hidden-lg"> | |||
{%= frappe.user.full_name(data.owner) %}, | |||
</span> | |||
{%= comment_when(data.modified) %} | |||
</div> | |||
{% if (data.owner==user) { %} | |||
<div> | |||
<a class="delete text-extra-muted" data-name="{%= data.name %}" | |||
onclick="frappe.pages.chat.chat.delete(this)">Delete</a> | |||
</div> | |||
{% } %} | |||
</div> | |||
</div> |
@@ -1,14 +0,0 @@ | |||
<ul class="nav nav-pills nav-stacked"> | |||
{% for (var i=0, l= data.length; i < l; i++) { var contact = data[i]; %} | |||
<li data-user="{%= contact.name %}" class="h6 module-sidebar-item"> | |||
<a class="messages-sidebar-link ellipsis"> | |||
<span class="indicator {% if(contact.has_session > 0) { %} green {% } else { %} grey {% } %}"> | |||
<span class="avatar avatar-small hidden-sm hidden-md hidden-lg" title="{%= frappe.user.full_name(contact.name) %} "> | |||
<img class="media-object" src="{%= frappe.user.image(contact.name) %}"> | |||
</span> | |||
<span class="hidden-xs">{%= contact.name===user ? __("Everyone") : frappe.user.full_name(contact.name) %}</span> | |||
</span> | |||
</a> | |||
</li> | |||
{% } %} | |||
</ul> |
@@ -63,6 +63,8 @@ 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"] | |||
# login | |||
@@ -185,6 +187,10 @@ sounds = [ | |||
{"name": "error", "src": "/assets/frappe/sounds/error.mp3", "volume": 0.1}, | |||
# {"name": "alert", "src": "/assets/frappe/sounds/alert.mp3"}, | |||
# {"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-noticication.mp3", "volume": 0.1 } | |||
] | |||
bot_parsers = [ | |||
@@ -8,4 +8,5 @@ Desk | |||
Integrations | |||
Printing | |||
Contacts | |||
Data Migration | |||
Data Migration | |||
Chat |
@@ -3,7 +3,9 @@ | |||
"public/css/font-awesome.css", | |||
"public/css/octicons/octicons.css", | |||
"public/css/website.css", | |||
"public/css/avatar.css" | |||
"public/css/avatar.css", | |||
"public/css/chat.css" | |||
], | |||
"js/frappe-web.min.js": [ | |||
"public/js/frappe/class.js", | |||
@@ -20,7 +22,11 @@ | |||
"public/js/lib/microtemplate.js", | |||
"public/js/frappe/query_string.js", | |||
"website/js/website.js", | |||
"public/js/frappe/misc/rating_icons.html" | |||
"public/js/frappe/misc/rating_icons.html", | |||
"public/js/lib/hyper.min.js", | |||
"public/js/lib/fuse.min.js", | |||
"public/js/frappe/chat.js" | |||
], | |||
"js/control.min.js": [ | |||
"public/js/frappe/ui/capture.js", | |||
@@ -124,7 +130,7 @@ | |||
"public/css/mobile.css", | |||
"public/css/kanban.css", | |||
"public/css/controls.css", | |||
"public/css/tags.css" | |||
"public/css/chat.css" | |||
], | |||
"css/frappe-rtl.css": [ | |||
"public/css/bootstrap-rtl.css", | |||
@@ -146,11 +152,13 @@ | |||
"public/js/lib/datepicker/datepicker.min.js", | |||
"public/js/lib/datepicker/locale-all.js", | |||
"public/js/lib/frappe-charts/frappe-charts.min.iife.js", | |||
"public/js/lib/webcam.min.js", | |||
"public/js/lib/leaflet/leaflet.js", | |||
"public/js/lib/leaflet/leaflet.draw.js", | |||
"public/js/lib/leaflet/L.Control.Locate.js", | |||
"public/js/lib/leaflet/easy-button.js" | |||
"public/js/lib/leaflet/easy-button.js", | |||
"public/js/lib/hyper.min.js", | |||
"public/js/lib/fuse.min.js" | |||
], | |||
"js/desk.min.js": [ | |||
"public/js/frappe/class.js", | |||
@@ -247,7 +255,9 @@ | |||
"public/js/frappe/ui/comment.js", | |||
"public/js/frappe/misc/rating_icons.html", | |||
"public/js/frappe/feedback.js" | |||
"public/js/frappe/feedback.js", | |||
"public/js/frappe/chat.js" | |||
], | |||
"css/module.min.css": [ | |||
"public/css/module.css" | |||
@@ -0,0 +1,121 @@ | |||
.font-bold { | |||
font-weight: 700; | |||
} | |||
.font-heavy { | |||
font-weight: 900; | |||
} | |||
.cursor-pointer { | |||
cursor: pointer; | |||
} | |||
.avatar { | |||
padding: 1px; | |||
} | |||
.frappe-fab { | |||
width: 48px; | |||
height: 48px; | |||
border-radius: 50%; | |||
box-shadow: 0px 3px 6px 0px rgba(0, 0, 0, 0.25); | |||
} | |||
.frappe-fab:hover { | |||
box-shadow: 0px 5px 9px 0px rgba(0, 0, 0, 0.25); | |||
} | |||
.frappe-fab.frappe-fab-sm { | |||
width: 40px; | |||
height: 40px; | |||
} | |||
.frappe-fab.frappe-fab-lg { | |||
width: 56px; | |||
height: 56px; | |||
} | |||
.frappe-chat .panel { | |||
margin-bottom: 0px !important; | |||
} | |||
.frappe-chat .panel .panel-heading { | |||
box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.14); | |||
} | |||
.frappe-chat .panel .panel-body { | |||
padding: 10px; | |||
} | |||
.frappe-chat .panel .frappe-chat-room-footer { | |||
position: absolute; | |||
bottom: 0px; | |||
} | |||
.frappe-chat .frappe-chat-form .form-control { | |||
font-size: 12px; | |||
} | |||
.frappe-chat .frappe-chat-form .dropdown-menu { | |||
border-radius: 4px; | |||
} | |||
.frappe-chat .frappe-chat-form .btn { | |||
border-radius: 0px !important; | |||
} | |||
.frappe-chat .frappe-chat-form .list-group { | |||
margin-bottom: 0px !important; | |||
max-height: 150px; | |||
overflow-y: auto; | |||
} | |||
.frappe-chat .frappe-chat-form .list-group .list-group-item:first-child, | |||
.frappe-chat .frappe-chat-form .list-group .list-group-item:last-child { | |||
border-radius: 0px !important; | |||
} | |||
.frappe-chat-popper { | |||
position: fixed; | |||
bottom: 0px; | |||
right: 0px; | |||
margin: 20px; | |||
z-index: 1035; | |||
} | |||
.frappe-chat-popper .frappe-chat-popper-collapse { | |||
position: fixed; | |||
bottom: 0px; | |||
right: 0px; | |||
margin: 20px; | |||
} | |||
.frappe-chat-popper .frappe-chat-popper-collapse > .panel { | |||
position: relative; | |||
box-shadow: 0px 3px 6px 0px rgba(0, 0, 0, 0.25); | |||
width: 350px; | |||
height: 500px; | |||
overflow-y: auto; | |||
} | |||
.frappe-chat-popper .frappe-chat-popper-collapse > .panel .panel-body { | |||
width: 350px; | |||
height: 500px; | |||
overflow-y: auto; | |||
} | |||
.frappe-chat-popper .frappe-chat-popper-collapse > .panel > .panel-heading { | |||
box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.14); | |||
} | |||
.frappe-chat-popper .frappe-chat-popper-collapse > .panel > .panel-heading .action { | |||
padding: 5px; | |||
} | |||
.frappe-chat-popper .frappe-chat-popper-collapse > .panel > .panel-heading a { | |||
color: #FFF; | |||
} | |||
.frappe-chat-popper .frappe-chat-popper-collapse > .panel > .panel-heading .text-muted { | |||
color: #FFF !important; | |||
} | |||
.frappe-chat-popper .frappe-chat-popper-collapse .panel-span { | |||
position: fixed; | |||
width: 100%; | |||
height: 100%; | |||
top: 0px; | |||
left: 0px; | |||
bottom: 0px; | |||
right: 0px; | |||
z-index: 1037; | |||
overflow: auto; | |||
border-radius: none; | |||
} | |||
.frappe-chat-emoji .dropdown-menu { | |||
min-width: 250px; | |||
background: none !important; | |||
border: none !important; | |||
} | |||
.frappe-chat-emoji .panel { | |||
margin-bottom: 0 !important; | |||
height: 300px; | |||
} | |||
.frappe-chat-emoji .panel .form-group { | |||
margin-bottom: 0 !important; | |||
} |
@@ -45,7 +45,7 @@ | |||
.list-id { | |||
margin-left: 7px !important; | |||
} | |||
.avatar-small { | |||
.avatar-small .avatar-sm { | |||
margin-left: 5px; | |||
margin-right: auto; | |||
} | |||
@@ -18,6 +18,12 @@ $(document).ready(function() { | |||
}); | |||
} | |||
frappe.start_app(); | |||
// frappe.Chat | |||
// Removing it from here as per rushabh@frappe.io's request. | |||
// const chat = new frappe.Chat() | |||
// chat.render(); | |||
// end frappe.Chat | |||
}); | |||
frappe.Application = Class.extend({ | |||
@@ -29,7 +35,7 @@ frappe.Application = Class.extend({ | |||
this.startup(); | |||
}, | |||
startup: function() { | |||
frappe.socketio.init(); | |||
frappe.socketio.init() | |||
frappe.model.init(); | |||
if(frappe.boot.status==='failed') { | |||
@@ -19,9 +19,9 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ | |||
tooltip: 'Camera', | |||
click: () => { | |||
const capture = new frappe.ui.Capture(); | |||
capture.open(); | |||
capture.show(); | |||
capture.click((data) => { | |||
capture.submit((data) => { | |||
context.invoke('editor.insertImage', data); | |||
}); | |||
} | |||
@@ -58,6 +58,10 @@ frappe.ui.form.Grid = Class.extend({ | |||
this.wrapper.find(".grid-add-row").click(function() { | |||
me.add_new_row(null, null, true); | |||
me.set_focus_on_row(); | |||
// excel like paste. | |||
me.setup_paste(); | |||
return false; | |||
}); | |||
@@ -71,7 +75,69 @@ frappe.ui.form.Grid = Class.extend({ | |||
this.df.on_setup(this); | |||
} | |||
this.setup_paste(); | |||
}, | |||
setup_paste: function ( ) { | |||
const me = this; | |||
$(document).ready(function ( ) { | |||
const $grid = $(me.wrapper).find('.form-grid'); | |||
const $rows = $grid.find('.grid-body .rows'); | |||
const $areas = $rows.find('.field-area'); | |||
$areas.click(function ( ) { | |||
// Who even comes up with this BS? | |||
if ( $(this).css('display') === 'block' ) { | |||
const $input = $(this).find('input'); | |||
$input.on('paste', function (e) { | |||
setTimeout(function ( ) { | |||
const value = e.target.value; | |||
// Do stuff here. | |||
const rows = value.split(' '); | |||
if ( rows.length ) { | |||
const first = rows[0]; | |||
// Who cares validating? They'll do it for us. | |||
const $area = $rows.find('.field-area').last(); | |||
if ( $area.css('display') === 'block' ) { | |||
const $input = $area.find('input'); | |||
$input.val(first); | |||
} | |||
for (var i = 1 ; i < rows.length ; ++i) { | |||
me.add_new_row(null, null, true); | |||
me.set_focus_on_row(); | |||
// const $area = $rows.find('.field-area').last(); | |||
// if ( $area.css('display') === 'block' ) { | |||
// const $input = $area.find('input'); | |||
// $input.val(rows[i]); | |||
// } | |||
} | |||
} | |||
// for (var i = 1 ; i < rows.length - 1 ; ++i) { | |||
// // // Assuming only single column. | |||
// // const elements = rows[i]; | |||
// // me.add_new_row(null, null, true); | |||
// // me.set_focus_on_row(); | |||
// // // const grid_row = me.grid_rows[me.grid_rows.length - rows.length]; | |||
// // // console.log(me.grid_rows.length); | |||
// // const grid_row = me.grid_rows[me.grid_rows.length - 1]; | |||
// // console.log(me.grid_rows.length - 1); | |||
// } | |||
// end stuff | |||
}, 100); // When you can't even | |||
}); | |||
} | |||
}); | |||
}); | |||
}, | |||
setup_check: function() { | |||
var me = this; | |||
this.wrapper.on('click', '.grid-row-check', function(e) { | |||
@@ -82,7 +82,7 @@ frappe.get_abbr = function(txt, max_length) { | |||
// continue | |||
return true; | |||
} | |||
87 | |||
abbr += w.trim()[0]; | |||
}); | |||
@@ -0,0 +1,15 @@ | |||
frappe.Peer = class | |||
{ | |||
constructor ( ) | |||
{ | |||
this.peer = new Peer() | |||
this.peer.on('open', (ID) => { | |||
console.log(`A new peer connection has occured with ID: ${ID}`) | |||
}) | |||
} | |||
} | |||
frappe.Peer.boot = ( ) => | |||
{ | |||
const client = new frappe.Peer() | |||
} |
@@ -10,6 +10,7 @@ frappe.request.waiting_for_ajax = []; | |||
// generic server call (call page, object) | |||
frappe.call = function(opts) { | |||
if (typeof arguments[0]==='string') { | |||
opts = { | |||
method: arguments[0], | |||
@@ -163,6 +163,7 @@ frappe.socketio = { | |||
// notify that the user has closed this doc | |||
frappe.socketio.socket.emit('doc_close', doctype, docname); | |||
}, | |||
setup_listeners: function() { | |||
frappe.socketio.socket.on('task_status_change', function(data) { | |||
frappe.socketio.process_response(data, data.status.toLowerCase()); | |||
@@ -390,5 +391,4 @@ frappe.socketio.SocketIOUploader = class SocketIOUploader { | |||
} | |||
} | |||
} | |||
} |
@@ -1,94 +1,204 @@ | |||
frappe.ui.Capture = class | |||
// frappe.ui.Capture | |||
// Author - Achilles Rasquinha <achilles@frappe.io> | |||
/** | |||
* @description Converts a canvas, image or a video to a data URL string. | |||
* | |||
* @param {HTMLElement} element - canvas, img or video. | |||
* @returns {string} - The data URL string. | |||
* | |||
* @example | |||
* frappe._.get_data_uri(video) | |||
* // returns "data:image/pngbase64,..." | |||
*/ | |||
frappe._.get_data_uri = element => | |||
{ | |||
constructor (options = { }) | |||
{ | |||
this.options = Object.assign({}, frappe.ui.Capture.DEFAULT_OPTIONS, options); | |||
this.dialog = new frappe.ui.Dialog(); | |||
this.template = | |||
` | |||
<div class="text-center"> | |||
<div class="img-thumbnail" style="border: none;"> | |||
<div id="frappe-capture"/> | |||
</div> | |||
</div> | |||
const $element = $(element) | |||
const width = $element.width() | |||
const height = $element.height() | |||
<div id="frappe-capture-btn-toolbar" style="padding-top: 15px; padding-bottom: 15px;"> | |||
<div class="text-center"> | |||
<div id="frappe-capture-btn-toolbar-snap"> | |||
<a id="frappe-capture-btn-snap"> | |||
<i class="fa fa-fw fa-2x fa-circle-o"/> | |||
</a> | |||
</div> | |||
<div class="btn-group" id="frappe-capture-btn-toolbar-knap"> | |||
<button class="btn btn-default" id="frappe-capture-btn-discard"> | |||
<i class="fa fa-fw fa-arrow-left"/> | |||
</button> | |||
<button class="btn btn-default" id="frappe-capture-btn-accept"> | |||
<i class="fa fa-fw fa-arrow-right"/> | |||
</button> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
$(this.dialog.body).append(this.template); | |||
const $canvas = $('<canvas/>') | |||
$canvas[0].width = width | |||
$canvas[0].height = height | |||
this.$btnBarSnap = $(this.dialog.body).find('#frappe-capture-btn-toolbar-snap'); | |||
this.$btnBarKnap = $(this.dialog.body).find('#frappe-capture-btn-toolbar-knap'); | |||
this.$btnBarKnap.hide(); | |||
const context = $canvas[0].getContext('2d') | |||
context.drawImage($element[0], 0, 0, width, height) | |||
const data_uri = $canvas[0].toDataURL('image/png') | |||
Webcam.set(this.options); | |||
} | |||
return data_uri | |||
} | |||
open ( ) | |||
/** | |||
* @description Frappe's Capture object. | |||
* | |||
* @example | |||
* const capture = frappe.ui.Capture() | |||
* capture.show() | |||
* | |||
* capture.click((data_uri) => { | |||
* // do stuff | |||
* }) | |||
* | |||
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Taking_still_photos | |||
*/ | |||
frappe.ui.Capture = class | |||
{ | |||
constructor (options = { }) | |||
{ | |||
this.dialog.show(); | |||
Webcam.attach('#frappe-capture'); | |||
this.options = frappe.ui.Capture.OPTIONS | |||
this.set_options(options) | |||
} | |||
freeze ( ) | |||
set_options (options) | |||
{ | |||
this.$btnBarSnap.hide(); | |||
this.$btnBarKnap.show(); | |||
this.options = { ...frappe.ui.Capture.OPTIONS, ...options } | |||
Webcam.freeze(); | |||
return this | |||
} | |||
unfreeze ( ) | |||
render ( ) | |||
{ | |||
this.$btnBarSnap.show(); | |||
this.$btnBarKnap.hide(); | |||
return navigator.mediaDevices.getUserMedia({ video: true }).then(stream => | |||
{ | |||
this.dialog = new frappe.ui.Dialog({ | |||
title: this.options.title, | |||
animate: this.options.animate, | |||
action: | |||
{ | |||
secondary: | |||
{ | |||
label: "<b>×</b>" | |||
} | |||
} | |||
}) | |||
const $e = $(frappe.ui.Capture.TEMPLATE) | |||
const video = $e.find('video')[0] | |||
video.srcObject = stream | |||
video.play() | |||
const $container = $(this.dialog.body) | |||
$container.html($e) | |||
$e.find('.fc-btf').hide() | |||
Webcam.unfreeze(); | |||
} | |||
$e.find('.fc-bcp').click(() => | |||
{ | |||
const data_url = frappe._.get_data_uri(video) | |||
$e.find('.fc-p').attr('src', data_url) | |||
click (callback) | |||
{ | |||
$(this.dialog.body).find('#frappe-capture-btn-snap').click(() => { | |||
this.freeze(); | |||
$e.find('.fc-s').hide() | |||
$e.find('.fc-p').show() | |||
$e.find('.fc-btu').hide() | |||
$e.find('.fc-btf').show() | |||
}) | |||
$e.find('.fc-br').click(() => | |||
{ | |||
$e.find('.fc-p').hide() | |||
$e.find('.fc-s').show() | |||
$(this.dialog.body).find('#frappe-capture-btn-discard').click(() => { | |||
this.unfreeze(); | |||
}); | |||
$e.find('.fc-btf').hide() | |||
$e.find('.fc-btu').show() | |||
}) | |||
$e.find('.fc-bs').click(() => | |||
{ | |||
const data_url = frappe._.get_data_uri(video) | |||
this.hide() | |||
if (this.callback) | |||
this.callback(data_url) | |||
}) | |||
}) | |||
} | |||
$(this.dialog.body).find('#frappe-capture-btn-accept').click(() => { | |||
Webcam.snap((data) => { | |||
callback(data); | |||
}); | |||
show ( ) | |||
{ | |||
this.render().then(() => | |||
{ | |||
this.dialog.show() | |||
}).catch(err => { | |||
if ( this.options.error ) | |||
{ | |||
const alert = `<span class="indicator red"/> ${frappe.ui.Capture.ERR_MESSAGE}` | |||
frappe.show_alert(alert, 3) | |||
} | |||
this.hide(); | |||
}); | |||
}); | |||
throw err | |||
}) | |||
} | |||
hide ( ) | |||
{ | |||
Webcam.reset(); | |||
if ( this.dialog ) | |||
this.dialog.hide() | |||
} | |||
$(this.dialog.$wrapper).remove(); | |||
submit (fn) | |||
{ | |||
this.callback = fn | |||
} | |||
}; | |||
frappe.ui.Capture.DEFAULT_OPTIONS = | |||
} | |||
frappe.ui.Capture.OPTIONS = | |||
{ | |||
width: 480, height: 320, flip_horiz: true | |||
}; | |||
title: __(`Camera`), | |||
animate: false, | |||
error: false, | |||
} | |||
frappe.ui.Capture.ERR_MESSAGE = __("Unable to load camera.") | |||
frappe.ui.Capture.TEMPLATE = | |||
` | |||
<div class="frappe-capture"> | |||
<div class="panel panel-default"> | |||
<img class="fc-p img-responsive"/> | |||
<div class="fc-s embed-responsive embed-responsive-16by9"> | |||
<video class="embed-responsive-item">${frappe.ui.Capture.ERR_MESSAGE}</video> | |||
</div> | |||
</div> | |||
<div> | |||
<div class="fc-btf"> | |||
<div class="row"> | |||
<div class="col-md-6"> | |||
<div class="pull-left"> | |||
<button class="btn btn-default fc-br"> | |||
<small>${__('Retake')}</small> | |||
</button> | |||
</div> | |||
</div> | |||
<div class="col-md-6"> | |||
<div class="pull-right"> | |||
<button class="btn btn-primary fc-bs"> | |||
<small>${__('Submit')}</small> | |||
</button> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="fc-btu"> | |||
<div class="row"> | |||
<div class="col-md-6"> | |||
${ | |||
'' | |||
// <div class="pull-left"> | |||
// <button class="btn btn-default"> | |||
// <small>${__('Take Video')}</small> | |||
// </button> | |||
// </div> | |||
} | |||
</div> | |||
<div class="col-md-6"> | |||
<div class="pull-right"> | |||
<button class="btn btn-default fc-bcp"> | |||
<small>${__('Take Photo')}</small> | |||
</button> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
` |
@@ -11,14 +11,20 @@ frappe.ui.Dialog = frappe.ui.FieldGroup.extend({ | |||
this.display = false; | |||
this.is_dialog = true; | |||
$.extend(this, opts); | |||
$.extend(this, { animate: true, size: null }, opts); | |||
this._super(); | |||
this.make(); | |||
}, | |||
make: function() { | |||
this.$wrapper = frappe.get_modal("", ""); | |||
this.wrapper = this.$wrapper.find('.modal-dialog') | |||
.get(0); | |||
if ( this.size == "small" ) | |||
$(this.wrapper).addClass("modal-sm"); | |||
else if ( this.size == "large" ) | |||
$(this.wrapper).addClass("modal-lg"); | |||
this.make_head(); | |||
this.body = this.$wrapper.find(".modal-body").get(0); | |||
this.header = this.$wrapper.find(".modal-header"); | |||
@@ -27,12 +33,13 @@ frappe.ui.Dialog = frappe.ui.FieldGroup.extend({ | |||
this._super(); | |||
// show footer | |||
if(this.primary_action) { | |||
this.set_primary_action(this.primary_action_label || __("Submit"), this.primary_action); | |||
this.action = this.action || { primary: { }, secondary: { } }; | |||
if(this.primary_action || this.action.primary) { | |||
this.set_primary_action(this.primary_action_label || this.action.primary.label || __("Submit"), this.primary_action || this.action.primary.click); | |||
} | |||
if (this.secondary_action_label) { | |||
this.get_close_btn().html(this.secondary_action_label); | |||
if (this.secondary_action_label || this.action.secondary.label) { | |||
this.get_close_btn().html(this.secondary_action_label || this.action.secondary.label); | |||
} | |||
var me = this; | |||
@@ -101,6 +108,11 @@ frappe.ui.Dialog = frappe.ui.FieldGroup.extend({ | |||
}, | |||
show: function() { | |||
// show it | |||
if ( this.animate ) { | |||
this.$wrapper.addClass('fade') | |||
} else { | |||
this.$wrapper.removeClass('fade') | |||
} | |||
this.$wrapper.modal("show"); | |||
this.primary_action_fulfilled = false; | |||
this.is_visible = true; | |||
@@ -45,6 +45,7 @@ | |||
</ul> | |||
</li> | |||
<!-- | |||
<li class="dropdown dropdown-help dropdown-mobile"> | |||
<a class="dropdown-toggle" data-toggle="dropdown" href="#" | |||
onclick="return false;" style="height: 40px;"> | |||
@@ -68,6 +69,17 @@ | |||
{%= __("About") %}</a></li> | |||
</ul> | |||
</li> | |||
--> | |||
<!-- Frappe Chat --> | |||
<li class="dropdown"> | |||
<a class="dropdown-toggle frappe-chat-btn" style="height: 40px; text-align: center;"> | |||
<div> | |||
<i class="octicon octicon-comment" style="margin-top: 5px;"/> | |||
</div> | |||
</a> | |||
</li> | |||
<!-- end Frappe Chat --> | |||
<li class="dropdown dropdown-navbar-new-comments dropdown-mobile"> | |||
<a class="btn dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> | |||
@@ -21,12 +21,23 @@ frappe.ui.toolbar.Toolbar = Class.extend({ | |||
make: function() { | |||
this.setup_sidebar(); | |||
this.setup_help(); | |||
// Frappe Chat (added to toolbar as per rushabh@frappe.io request) | |||
this.setup_chat() | |||
// end Frappe Chat | |||
this.setup_progress_dialog(); | |||
this.bind_events(); | |||
$(document).trigger('toolbar_setup'); | |||
}, | |||
setup_chat ( ) | |||
{ | |||
const chat = new frappe.Chat({ target: '.navbar .frappe-chat-btn' }) | |||
chat.render() | |||
}, | |||
bind_events: function() { | |||
$(document).on("notification-update", function() { | |||
frappe.ui.notifications.update_notifications(); | |||
@@ -159,7 +159,7 @@ frappe.setup.UserProgressDialog = class UserProgressDialog { | |||
frappe.call({ | |||
method: "frappe.desk.user_progress.update_and_get_user_progress", | |||
callback: function(r) { | |||
// console.log("states", r.message); | |||
console.log("states", r.message); | |||
let states = r.message; | |||
let changed = 0; | |||
let completed = 0; | |||
@@ -0,0 +1,225 @@ | |||
// variables | |||
@color-white: #FFF; // | |||
@font-weight-bold: 700; // | |||
@font-weight-heavy: 900; // | |||
@frappe-chat-popper-panel-width: 350px; // | |||
@frappe-chat-popper-panel-height: 500px; // | |||
@frappe-chat-form-font-size: 12px; | |||
@frappe-fab-width: 48px; | |||
@frappe-fab-height: 48px; | |||
@frappe-fab-lg: 56px; | |||
@frappe-fab-sm: 40px; | |||
// https://github.com/twbs/bootstrap/blob/v3.3.7/less/variables.less#L278 | |||
// Keep z-index of the FAB button higher than others, lower than modal background. | |||
@frappe-fab-box-shadow: 0px 3px 6px 0px rgba(0,0,0,.25); | |||
@frappe-fab-box-shadow-hover: 0px 5px 9px 0px rgba(0,0,0,.25); | |||
@frappe-chat-panel-heading-box-shadow: 0px 2px 2px 0px rgba(0,0,0,.14); // | |||
@frappe-chat-panel-body-padding: 10px; | |||
@frappe-chat-panel-heading-action-padding: 5px; | |||
@frappe-chat-popper-z-index: 1035; | |||
@frappe-chat-popper-margin: 20px; | |||
@frappe-chat-popper-panel-box-shadow: @frappe-fab-box-shadow; | |||
// z-index greater than FAB, lesser than modal. | |||
@frappe-chat-popper-panel-span-z-index: 1037; | |||
@frappe-chat-form-list-group-height: 150px; | |||
@frappe-chat-form-menu-border-radius: 4px; | |||
@frappe-chat-emoji-width: 250px; | |||
@frappe-chat-emoji-height: 300px; | |||
.font-bold | |||
{ | |||
font-weight: @font-weight-bold; | |||
} | |||
.font-heavy | |||
{ | |||
font-weight: @font-weight-heavy; | |||
} | |||
.cursor-pointer | |||
{ | |||
cursor: pointer; | |||
} | |||
.avatar | |||
{ | |||
padding: 1px; | |||
} | |||
.frappe-fab | |||
{ | |||
width: @frappe-fab-width; | |||
height: @frappe-fab-height; | |||
border-radius: 50%; | |||
box-shadow: @frappe-fab-box-shadow; | |||
&:hover | |||
{ | |||
box-shadow: @frappe-fab-box-shadow-hover; | |||
}; | |||
&.frappe-fab-sm | |||
{ | |||
width: @frappe-fab-sm; | |||
height: @frappe-fab-sm; | |||
}; | |||
&.frappe-fab-lg | |||
{ | |||
width: @frappe-fab-lg; | |||
height: @frappe-fab-lg; | |||
}; | |||
}; | |||
.frappe-chat | |||
{ | |||
.panel | |||
{ | |||
margin-bottom: 0px !important; | |||
.panel-heading | |||
{ | |||
box-shadow: @frappe-chat-panel-heading-box-shadow; | |||
} | |||
.panel-body | |||
{ | |||
padding: @frappe-chat-panel-body-padding; | |||
} | |||
.frappe-chat-room-footer | |||
{ | |||
position: absolute; | |||
bottom: 0px; | |||
} | |||
} | |||
.frappe-chat-form | |||
{ | |||
.form-control | |||
{ | |||
font-size: @frappe-chat-form-font-size; | |||
} | |||
.dropdown-menu | |||
{ | |||
border-radius: @frappe-chat-form-menu-border-radius; | |||
} | |||
.btn | |||
{ | |||
border-radius: 0px !important; | |||
} | |||
.list-group | |||
{ | |||
margin-bottom: 0px !important; | |||
max-height: @frappe-chat-form-list-group-height; | |||
overflow-y: auto; | |||
.list-group-item:first-child, .list-group-item:last-child | |||
{ | |||
border-radius: 0px !important; | |||
} | |||
} | |||
} | |||
} | |||
.frappe-chat-popper | |||
{ | |||
position: fixed; | |||
bottom: 0px; | |||
right: 0px; | |||
margin: @frappe-chat-popper-margin; | |||
z-index: @frappe-chat-popper-z-index; | |||
.frappe-chat-popper-collapse | |||
{ | |||
position: fixed; | |||
bottom: 0px; | |||
right: 0px; | |||
margin: @frappe-chat-popper-margin; | |||
// margin-bottom: calc(@frappe-chat-popper-margin + 50px); | |||
& > .panel | |||
{ | |||
position: relative; | |||
box-shadow: @frappe-chat-popper-panel-box-shadow; | |||
width: @frappe-chat-popper-panel-width; | |||
height: @frappe-chat-popper-panel-height; | |||
overflow-y: auto; | |||
.panel-body | |||
{ | |||
width: @frappe-chat-popper-panel-width; | |||
height: @frappe-chat-popper-panel-height; | |||
overflow-y: auto; | |||
} | |||
& > .panel-heading | |||
{ | |||
box-shadow: @frappe-chat-panel-heading-box-shadow; | |||
.action | |||
{ | |||
padding: @frappe-chat-panel-heading-action-padding; | |||
} | |||
a | |||
{ | |||
color: @color-white; | |||
} | |||
.text-muted | |||
{ | |||
color: @color-white !important; | |||
} | |||
} | |||
}; | |||
.panel-span | |||
{ | |||
position: fixed; | |||
width: 100%; | |||
height: 100%; | |||
top: 0px; | |||
left: 0px; | |||
bottom: 0px; | |||
right: 0px; | |||
z-index: @frappe-chat-popper-panel-span-z-index; | |||
overflow: auto; | |||
border-radius: none; | |||
}; | |||
}; | |||
}; | |||
.frappe-chat-emoji | |||
{ | |||
.dropdown-menu | |||
{ | |||
min-width: @frappe-chat-emoji-width; | |||
background: none !important; | |||
border: none !important; | |||
} | |||
.panel | |||
{ | |||
margin-bottom: 0 !important; | |||
height: @frappe-chat-emoji-height; | |||
.form-group | |||
{ | |||
margin-bottom: 0 !important; | |||
} | |||
} | |||
}; |
@@ -114,6 +114,7 @@ login.signup = function() { | |||
// Login | |||
login.call = function(args, callback) { | |||
login.set_indicator("{{ _('Verifying...') }}", 'blue'); | |||
return frappe.call({ | |||
type: "POST", | |||
args: args, | |||
@@ -191,6 +191,7 @@ def _run_unittest(modules, verbose=False, tests=(), profile=False): | |||
out = unittest_runner(verbosity=1+(verbose and 1 or 0)).run(test_suite) | |||
if profile: | |||
pr.disable() | |||
s = StringIO() | |||
@@ -200,7 +201,6 @@ def _run_unittest(modules, verbose=False, tests=(), profile=False): | |||
return out | |||
def _add_test(app, path, filename, verbose, test_suite=None, ui_tests=False): | |||
import os | |||
@@ -408,3 +408,11 @@ $(document).on("page-change", function() { | |||
element && element.scrollIntoView(true); | |||
} | |||
}); | |||
$(document).ready(function ( ) { | |||
// frappe.Chat | |||
// const chat = new frappe.Chat(); | |||
// chat.render(); | |||
// end frappe.Chat | |||
}); |
@@ -117,7 +117,7 @@ frappe.ready(function() { | |||
method: 'frappe.core.doctype.user.user.test_password_strength', | |||
args: args, | |||
callback: function(r) { | |||
// console.log(r.message); | |||
console.log(r.message); | |||
}, | |||
statusCode: { | |||
401: function() { | |||
@@ -1,6 +1,6 @@ | |||
var app = require('express')(); | |||
var http = require('http').Server(app); | |||
var io = require('socket.io')(http); | |||
var server = require('http').Server(app); | |||
var io = require('socket.io')(server); | |||
var cookie = require('cookie') | |||
var fs = require('fs'); | |||
var path = require('path'); | |||
@@ -22,117 +22,151 @@ var files_struct = { | |||
var subscriber = redis.createClient(conf.redis_socketio || conf.redis_async_broker_port); | |||
// serve socketio | |||
http.listen(conf.socketio_port, function() { | |||
console.log('listening on *:', conf.socketio_port); //eslint-disable-line | |||
server.listen(conf.socketio_port, function () { | |||
console.log('listening on *:', conf.socketio_port); //eslint-disable-line | |||
}); | |||
// test route | |||
app.get('/', function(req, res) { | |||
app.get('/', function (req, res) { | |||
res.sendfile('index.html'); | |||
}); | |||
// on socket connection | |||
io.on('connection', function(socket) { | |||
io.on('connection', function (socket) { | |||
console.log(socket.request.headers) | |||
if (get_hostname(socket.request.headers.host) != get_hostname(socket.request.headers.origin)) { | |||
return; | |||
} | |||
// console.log("connection!"); | |||
if (!socket.request.headers.cookie) { | |||
return; | |||
} | |||
var sid = cookie.parse(socket.request.headers.cookie).sid | |||
if(!sid) { | |||
if (!sid) { | |||
return; | |||
} | |||
if(flags[sid]) { | |||
if (flags[sid]) { | |||
// throttle this function | |||
return; | |||
} | |||
flags[sid] = sid; | |||
setTimeout(function() { flags[sid] = null; }, 10000); | |||
setTimeout(function () { | |||
flags[sid] = null; | |||
}, 10000); | |||
socket.user = cookie.parse(socket.request.headers.cookie).user_id; | |||
socket.files = {}; | |||
// console.log("firing get_user_info"); | |||
// frappe.chat | |||
socket.on("frappe.chat.room:subscribe", function (rooms) { | |||
if (!Array.isArray(rooms)) { | |||
rooms = [rooms]; | |||
} | |||
for (var room of rooms) { | |||
console.log('frappe.chat: Subscribing ' + socket.user + ' to room ' + room); | |||
room = get_chat_room(socket, room); | |||
console.log('frappe.chat: Subscribing ' + socket.user + ' to event ' + room); | |||
socket.join(room); | |||
} | |||
}); | |||
socket.on("frappe.chat.message:typing", function (data) { | |||
const user = data.user; | |||
const room = get_chat_room(socket, data.room); | |||
console.log('frappe.chat: Dispatching ' + user + ' typing to room ' + room); | |||
io.to(room).emit('frappe.chat.room:typing', { | |||
room: data.room, | |||
user: user | |||
}); | |||
}); | |||
console.log("firing get_user_info"); | |||
request.get(get_url(socket, '/api/method/frappe.async.get_user_info')) | |||
.type('form') | |||
.query({ | |||
sid: sid | |||
}) | |||
.end(function(err, res) { | |||
if(err) { | |||
.end(function (err, res) { | |||
if (err) { | |||
console.log(err); | |||
return; | |||
} | |||
if(res.status == 200) { | |||
if (res.status == 200) { | |||
var room = get_user_room(socket, res.body.message.user); | |||
// console.log('joining', room); | |||
console.log('joining', room); | |||
socket.join(room); | |||
socket.join(get_site_room(socket)); | |||
} | |||
}); | |||
socket.on('disconnect', function() { | |||
socket.on('disconnect', function () { | |||
delete socket.files; | |||
}) | |||
socket.on('task_subscribe', function(task_id) { | |||
socket.on('task_subscribe', function (task_id) { | |||
var room = get_task_room(socket, task_id); | |||
socket.join(room); | |||
}); | |||
socket.on('task_unsubscribe', function(task_id) { | |||
socket.on('task_unsubscribe', function (task_id) { | |||
var room = get_task_room(socket, task_id); | |||
socket.leave(room); | |||
}); | |||
socket.on('progress_subscribe', function(task_id) { | |||
socket.on('progress_subscribe', function (task_id) { | |||
var room = get_task_room(socket, task_id); | |||
socket.join(room); | |||
send_existing_lines(task_id, socket); | |||
}); | |||
socket.on('doc_subscribe', function(doctype, docname) { | |||
// console.log('trying to subscribe', doctype, docname) | |||
socket.on('doc_subscribe', function (doctype, docname) { | |||
console.log('trying to subscribe', doctype, docname) | |||
can_subscribe_doc({ | |||
socket: socket, | |||
sid: sid, | |||
doctype: doctype, | |||
docname: docname, | |||
callback: function(err, res) { | |||
callback: function (err, res) { | |||
var room = get_doc_room(socket, doctype, docname); | |||
// console.log('joining', room) | |||
console.log('joining', room) | |||
socket.join(room); | |||
} | |||
}); | |||
}); | |||
socket.on('doc_unsubscribe', function(doctype, docname) { | |||
socket.on('doc_unsubscribe', function (doctype, docname) { | |||
var room = get_doc_room(socket, doctype, docname); | |||
socket.leave(room); | |||
}); | |||
socket.on('task_unsubscribe', function(task_id) { | |||
socket.on('task_unsubscribe', function (task_id) { | |||
var room = 'task:' + task_id; | |||
socket.leave(room); | |||
}); | |||
socket.on('doc_open', function(doctype, docname) { | |||
socket.on('doc_open', function (doctype, docname) { | |||
// show who is currently viewing the form | |||
can_subscribe_doc({ | |||
socket: socket, | |||
sid: sid, | |||
doctype: doctype, | |||
docname: docname, | |||
callback: function(err, res) { | |||
callback: function (err, res) { | |||
var room = get_open_doc_room(socket, doctype, docname); | |||
// console.log('joining', room) | |||
console.log('joining', room) | |||
socket.join(room); | |||
send_viewers({ | |||
@@ -144,7 +178,7 @@ io.on('connection', function(socket) { | |||
}); | |||
}); | |||
socket.on('doc_close', function(doctype, docname) { | |||
socket.on('doc_close', function (doctype, docname) { | |||
// remove this user from the list of 'who is currently viewing the form' | |||
var room = get_open_doc_room(socket, doctype, docname); | |||
socket.leave(room); | |||
@@ -197,17 +231,18 @@ io.on('connection', function(socket) { | |||
}); | |||
}); | |||
subscriber.on("message", function(channel, message) { | |||
message = JSON.parse(message); | |||
io.to(message.room).emit(message.event, message.message); | |||
// console.log(message.room, message.event, message.message) | |||
subscriber.on("message", function (channel, message, room) { | |||
message = JSON.parse(message) | |||
console.log(message) | |||
io.to(message.room).emit(message.event, message.message) | |||
}); | |||
subscriber.subscribe("events"); | |||
function send_existing_lines(task_id, socket) { | |||
var room = get_task_room(socket, task_id); | |||
subscriber.hgetall('task_log:' + task_id, function(err, lines) { | |||
subscriber.hgetall('task_log:' + task_id, function (err, lines) { | |||
io.to(room).emit('task_progress', { | |||
"task_id": task_id, | |||
"message": { | |||
@@ -218,11 +253,11 @@ function send_existing_lines(task_id, socket) { | |||
} | |||
function get_doc_room(socket, doctype, docname) { | |||
return get_site_name(socket) + ':doc:'+ doctype + '/' + docname; | |||
return get_site_name(socket) + ':doc:' + doctype + '/' + docname; | |||
} | |||
function get_open_doc_room(socket, doctype, docname) { | |||
return get_site_name(socket) + ':open_doc:'+ doctype + '/' + docname; | |||
return get_site_name(socket) + ':open_doc:' + doctype + '/' + docname; | |||
} | |||
function get_user_room(socket, user) { | |||
@@ -237,19 +272,25 @@ function get_task_room(socket, task_id) { | |||
return get_site_name(socket) + ':task_progress:' + task_id; | |||
} | |||
// frappe.chat | |||
// If you're thinking on multi-site or anything, please | |||
// update frappe.async as well. | |||
function get_chat_room(socket, room) { | |||
var room = get_site_name(socket) + ":room:" + room; | |||
return room | |||
} | |||
function get_site_name(socket) { | |||
if (socket.request.headers['x-frappe-site-name']) { | |||
return get_hostname(socket.request.headers['x-frappe-site-name']); | |||
} | |||
else if (['localhost', '127.0.0.1'].indexOf(socket.request.headers.host) !== -1 | |||
&& conf.default_site) { | |||
} else if (['localhost', '127.0.0.1'].indexOf(socket.request.headers.host) !== -1 && | |||
conf.default_site) { | |||
// from currentsite.txt since host is localhost | |||
return conf.default_site; | |||
} | |||
else if (socket.request.headers.origin) { | |||
} else if (socket.request.headers.origin) { | |||
return get_hostname(socket.request.headers.origin); | |||
} | |||
else { | |||
} else { | |||
return get_hostname(socket.request.headers.host); | |||
} | |||
} | |||
@@ -259,7 +300,7 @@ function get_hostname(url) { | |||
if (url.indexOf("://") > -1) { | |||
url = url.split('/')[2]; | |||
} | |||
return ( url.match(/:/g) ) ? url.slice( 0, url.indexOf(":") ) : url | |||
return (url.match(/:/g)) ? url.slice(0, url.indexOf(":")) : url | |||
} | |||
function get_url(socket, path) { | |||
@@ -270,8 +311,8 @@ function get_url(socket, path) { | |||
} | |||
function can_subscribe_doc(args) { | |||
if(!args) return; | |||
if(!args.doctype || !args.docname) return; | |||
if (!args) return; | |||
if (!args.doctype || !args.docname) return; | |||
request.get(get_url(args.socket, '/api/method/frappe.async.can_subscribe_doc')) | |||
.type('form') | |||
.query({ | |||
@@ -279,7 +320,7 @@ function can_subscribe_doc(args) { | |||
doctype: args.doctype, | |||
docname: args.docname | |||
}) | |||
.end(function(err, res) { | |||
.end(function (err, res) { | |||
if (!res) { | |||
console.log("No response for doc_subscribe"); | |||
@@ -318,7 +359,7 @@ function send_viewers(args) { | |||
var viewers = []; | |||
for (var i in io.sockets.sockets) { | |||
var s = io.sockets.sockets[i]; | |||
if (clients.indexOf(s.id)!==-1) { | |||
if (clients.indexOf(s.id) !== -1) { | |||
// this socket is connected to the room | |||
viewers.push(s.user); | |||
} | |||
@@ -339,8 +380,8 @@ function get_conf() { | |||
socketio_port: 3000 | |||
}; | |||
var read_config = function(path) { | |||
if(fs.existsSync(path)){ | |||
var read_config = function (path) { | |||
if (fs.existsSync(path)) { | |||
var bench_config = JSON.parse(fs.readFileSync(path)); | |||
for (var key in bench_config) { | |||
if (bench_config[key]) { | |||
@@ -355,11 +396,9 @@ function get_conf() { | |||
read_config('sites/common_site_config.json'); | |||
// detect current site | |||
if(fs.existsSync('sites/currentsite.txt')) { | |||
if (fs.existsSync('sites/currentsite.txt')) { | |||
conf.default_site = fs.readFileSync('sites/currentsite.txt').toString().trim(); | |||
} | |||
return conf; | |||
} | |||
} |