[feature] Ability to like a document, comment or communication, see notifications about it and view it on activity feed

This commit is contained in:
Anand Doshi 2015-12-28 19:56:58 +05:30
parent e3a2b1265c
commit 3241a0969f
72 changed files with 1839 additions and 999 deletions

View file

@ -0,0 +1,4 @@
- Ability to **Like** a document, comment or communication
- See notifications about likes that you received
- View it on Activity feed
- *Stars* have been converted to Likes

View file

@ -1,338 +1,338 @@
{
"allow_copy": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "hash",
"creation": "2012-08-08 10:40:11",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"allow_copy": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "hash",
"creation": "2012-08-08 10:40:11",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Comment",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment",
"oldfieldtype": "Text",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Comment",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment",
"oldfieldtype": "Text",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_type",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Comment Type",
"length": 0,
"no_copy": 0,
"options": "Email\nChat\nPhone\nSMS\nCreated\nSubmitted\nCancelled\nAssigned\nAssignment Completed\nComment\nWorkflow\nLabel\nAttachment\nAttachment Removed",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_type",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Comment Type",
"length": 0,
"no_copy": 0,
"options": "Email\nChat\nPhone\nSMS\nCreated\nSubmitted\nCancelled\nAssigned\nAssignment Completed\nComment\nWorkflow\nLabel\nAttachment\nAttachment Removed\nLike",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_by",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Comment By",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_by",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_by",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Comment By",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_by",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_by_fullname",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Comment By Fullname",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_by_fullname",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_by_fullname",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Comment By Fullname",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_by_fullname",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Comment Date",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_date",
"oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Comment Date",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_date",
"oldfieldtype": "Date",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_time",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Comment Time",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_time",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_time",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Comment Time",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_time",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_doctype",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Comment Doctype",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_doctype",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_doctype",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Comment Doctype",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_doctype",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_docname",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Comment Docname",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_docname",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "comment_docname",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Comment Docname",
"length": 0,
"no_copy": 0,
"oldfieldname": "comment_docname",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "post_topic",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Post Topic",
"length": 0,
"no_copy": 0,
"oldfieldname": "post_topic",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "post_topic",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Post Topic",
"length": 0,
"no_copy": 0,
"oldfieldname": "post_topic",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "unsubscribed",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Unsubscribed",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "unsubscribed",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Unsubscribed",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "reference_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference DocType",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "reference_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference DocType",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"description": "Reference DocType and Reference Name are used to render a comment as a link (href) to a Doc.",
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference Name",
"length": 0,
"no_copy": 0,
"options": "reference_doctype",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"description": "Reference DocType and Reference Name are used to render a comment as a link (href) to a Doc.",
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference Name",
"length": 0,
"no_copy": 0,
"options": "reference_doctype",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "icon-comments",
"idx": 1,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2015-11-16 06:29:43.314568",
"modified_by": "Administrator",
"module": "Core",
"name": "Comment",
"owner": "Administrator",
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "icon-comments",
"idx": 1,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2015-12-26 06:29:43.314568",
"modified_by": "Administrator",
"module": "Core",
"name": "Comment",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"read_only": 0,
"read_only_onload": 0,
],
"read_only": 0,
"read_only_onload": 0,
"title_field": "comment"
}
}

View file

@ -23,8 +23,8 @@ class Comment(Document):
if self.comment_type in ("Created", "Submitted", "Cancelled", "Label"):
comment_type = "Label"
elif self.comment_type == "Comment":
comment_type = "Comment"
elif self.comment_type in ("Comment", "Like"):
comment_type = self.comment_type
else:
comment_type = "Info"
@ -32,7 +32,9 @@ class Comment(Document):
"subject": self.comment,
"doctype": self.comment_doctype,
"name": self.comment_docname,
"feed_type": comment_type
"feed_type": comment_type,
"reference_doctype": self.reference_doctype,
"reference_name": self.reference_name
}
def after_insert(self):
@ -182,3 +184,6 @@ def on_doctype_update():
frappe.db.commit()
frappe.db.sql("""alter table `tabComment`
add index comment_doctype_docname_index(comment_doctype, comment_docname)""")
if "_liked_by" not in frappe.db.get_table_columns("Comment"):
add_column("Comment", "_liked_by", "Text")

View file

@ -10,6 +10,7 @@ from frappe.utils.file_manager import get_file
from frappe.email.bulk import check_bulk_limit
import frappe.email.smtp
from frappe import _
from frappe.model.db_schema import add_column
from frappe.model.document import Document
@ -246,7 +247,6 @@ class Communication(Document):
# if it is a fetched email, add follows to CC
cc.append(self.get_owner_email())
cc += self.get_assignees()
cc += self.get_starrers()
if cc:
# exclude email accounts, unfollows, recipients and unsubscribes
@ -306,10 +306,6 @@ class Communication(Document):
return filtered
def get_starrers(self):
"""Return list of users who have starred this document."""
return [( get_formatted_email(user) or user ) for user in self.get_parent_doc().get_starred_by()]
def get_owner_email(self):
owner = self.get_parent_doc().owner
return get_formatted_email(owner) or owner
@ -337,6 +333,9 @@ def on_doctype_update():
"""Add index in `tabCommunication` for `(reference_doctype, reference_name)`"""
frappe.db.add_index("Communication", ["reference_doctype", "reference_name"])
if "_liked_by" not in frappe.db.get_table_columns("Communication"):
add_column("Communication", "_liked_by", "Text")
@frappe.whitelist()
def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
sender=None, recipients=None, communication_medium="Email", send_email=False,

View file

@ -14,6 +14,9 @@ def get_notification_config():
"Comment": "frappe.core.notifications.get_unread_messages",
"Error Snapshot": {"seen": 0, "parent_error_snapshot": None},
},
"for_other": {
"Likes": "frappe.core.notifications.get_unseen_likes"
}
}
def get_things_todo():
@ -41,3 +44,12 @@ def get_unread_messages():
AND comment_docname = %s
AND docstatus=0
""", (frappe.session.user,))[0][0]
def get_unseen_likes():
"""Returns count of unseen likes"""
return frappe.db.sql("""select count(*) from `tabFeed`
where
feed_type='Like'
and owner is not null and owner!=%(user)s
and doc_owner=%(user)s
and seen=0""", {"user": frappe.session.user})[0][0]

View file

@ -1,204 +1,309 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "hash",
"creation": "2012-07-03 13:29:42",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "hash",
"creation": "2012-07-03 13:29:42",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "feed_type",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Feed Type",
"length": 0,
"no_copy": 0,
"options": "\nComment\nLogin\nLabel\nInfo",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "feed_type",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Feed Type",
"length": 0,
"no_copy": 0,
"options": "\nComment\nLogin\nLabel\nInfo\nLike",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "doc_type",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Doc Type",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "doc_type",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Doc Type",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "doc_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Doc Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "doc_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Doc Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "subject",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Subject",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "subject",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Subject",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "color",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Color",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "color",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Color",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "full_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Full Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "full_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Full Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "doc_owner",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Doc Owner",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "seen",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Seen",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"description": "Use this to provide alternative link to a feed record",
"fieldname": "reference_doctype",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference DocType",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "reference_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "icon-rss",
"idx": 1,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2015-11-16 06:29:47.123186",
"modified_by": "Administrator",
"module": "Desk",
"name": "Feed",
"owner": "Administrator",
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "icon-rss",
"idx": 1,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2015-12-30 02:48:03.860188",
"modified_by": "Administrator",
"module": "Desk",
"name": "Feed",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
},
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 1,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "All",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"amend": 0,
"apply_user_permissions": 1,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 1,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "All",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}
],
"read_only": 0,
],
"read_only": 0,
"read_only_onload": 0
}
}

View file

@ -12,7 +12,13 @@ from frappe import _
exclude_from_linked_with = True
class Feed(Document):
pass
no_feed_on_delete = True
def validate(self):
if not (self.reference_doctype and self.reference_name):
# reset both if even one is missing
self.reference_doctype = self.reference_name = None
def on_doctype_update():
if not frappe.db.sql("""show index from `tabFeed`
@ -24,9 +30,12 @@ def on_doctype_update():
def get_permission_query_conditions(user):
if not user: user = frappe.session.user
if not frappe.permissions.apply_user_permissions("Feed", "read", user):
use_user_permissions = frappe.permissions.apply_user_permissions("Feed", "read", user)
if not use_user_permissions:
return ""
conditions = ['`tabFeed`.owner="{user}" or `tabFeed`.doc_owner="{user}"'.format(user=frappe.db.escape(user))]
user_permissions = frappe.defaults.get_user_permissions(user)
can_read = frappe.get_user().get_can_read()
@ -34,19 +43,17 @@ def get_permission_query_conditions(user):
list(set(can_read) - set(user_permissions.keys()))]
if not can_read_doctypes:
return ""
conditions += ["tabFeed.doc_type in ({})".format(", ".join(can_read_doctypes))]
conditions = ["tabFeed.doc_type in ({})".format(", ".join(can_read_doctypes))]
if user_permissions:
can_read_docs = []
for doctype, names in user_permissions.items():
for n in names:
can_read_docs.append('"{}|{}"'.format(doctype, n))
if user_permissions:
can_read_docs = []
for doctype, names in user_permissions.items():
for n in names:
can_read_docs.append('"{}|{}"'.format(doctype, n))
if can_read_docs:
conditions.append("concat_ws('|', tabFeed.doc_type, tabFeed.doc_name) in ({})".format(
", ".join(can_read_docs)))
if can_read_docs:
conditions.append("concat_ws('|', tabFeed.doc_type, tabFeed.doc_name) in ({})".format(
", ".join(can_read_docs)))
return "(" + " or ".join(conditions) + ")"
@ -83,7 +90,10 @@ def update_feed(doc, method=None):
"doc_type": doctype,
"doc_name": name,
"subject": feed.subject,
"full_name": get_fullname(doc.owner)
"full_name": get_fullname(doc.owner),
"doc_owner": frappe.db.get_value(doctype, name, "owner"),
"reference_doctype": feed.reference_doctype,
"reference_name": feed.reference_name
}).insert(ignore_permissions=True)
def login_feed(login_manager):

View file

@ -95,11 +95,11 @@ def get_docinfo(doc=None, doctype=None, name=None):
def get_user_permissions(meta):
out = {}
all_user_permissions = frappe.defaults.get_user_permissions()
for m in meta:
for df in m.get_fields_to_check_permissions(all_user_permissions):
out[df.options] = list(set(all_user_permissions[df.options]))
return out
def get_attachments(dt, dn):
@ -108,7 +108,7 @@ def get_attachments(dt, dn):
def get_comments(dt, dn, limit=100):
comments = frappe.db.sql("""select name, comment, comment_by, creation,
reference_doctype, reference_name, comment_type, "Comment" as doctype
reference_doctype, reference_name, comment_type, "Comment" as doctype, _liked_by
from `tabComment`
where comment_doctype=%s and comment_docname=%s
order by creation desc limit %s""",
@ -116,7 +116,7 @@ def get_comments(dt, dn, limit=100):
communications = frappe.db.sql("""select name,
content as comment, sender as comment_by, creation,
communication_medium as comment_type, subject, delivery_status,
communication_medium as comment_type, subject, delivery_status, _liked_by,
"Communication" as doctype
from tabCommunication
where reference_doctype=%s and reference_name=%s

98
frappe/desk/like.py Normal file
View file

@ -0,0 +1,98 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
"""Allow adding of likes to documents"""
import frappe, json
from frappe.model.db_schema import add_column
from frappe import _
from frappe.utils import get_link_to_form
@frappe.whitelist()
def toggle_like(doctype, name, add=False):
"""Adds / removes the current user in the `__liked_by` property of the given document.
If column does not exist, will add it in the database.
The `_liked_by` property is always set from this function and is ignored if set via
Document API
:param doctype: DocType of the document to like
:param name: Name of the document to like
:param add: `Yes` if like is to be added. If not `Yes` the like will be removed."""
_toggle_like(doctype, name, add)
def _toggle_like(doctype, name, add=False, user=None):
"""Same as toggle_like but hides param `user` from API"""
if not user:
user = frappe.session.user
try:
liked_by = frappe.db.get_value(doctype, name, "_liked_by")
if liked_by:
liked_by = json.loads(liked_by)
else:
liked_by = []
if add=="Yes":
if user not in liked_by:
liked_by.append(user)
add_comment(doctype, name)
else:
if user in liked_by:
liked_by.remove(user)
remove_like(doctype, name)
frappe.db.set_value(doctype, name, "_liked_by", json.dumps(liked_by), update_modified=False)
except Exception, e:
if e.args[0]==1054:
add_column(doctype, "_liked_by", "Text")
_toggle_like(doctype, name, add, user)
else:
raise
def remove_like(doctype, name):
"""Remove previous Like"""
# remove Comment
frappe.delete_doc("Comment", [c.name for c in frappe.get_all("Comment",
filters={
"comment_doctype": doctype,
"comment_docname": name,
"comment_by": frappe.session.user,
"comment_type": "Like"
}
)])
# remove Feed
frappe.delete_doc("Feed", [c.name for c in frappe.get_all("Feed",
filters={
"doc_type": doctype,
"doc_name": name,
"owner": frappe.session.user,
"feed_type": "Like"
}
)], ignore_permissions=True)
def add_comment(doctype, name):
doc = frappe.get_doc(doctype, name)
if doctype=="Comment":
link = get_link_to_form(doc.comment_doctype, doc.comment_docname,
"{0} {1}".format(_(doc.comment_doctype), doc.comment_docname))
doc.add_comment("Like", _("Comment: {0} in {1}").format("<b>" + doc.comment + "</b>", link),
reference_doctype=doc.comment_doctype, reference_name=doc.comment_docname)
elif doctype=="Communication":
link = get_link_to_form(doc.reference_doctype, doc.reference_name,
"{0} {1}".format(_(doc.reference_doctype), doc.reference_name))
doc.add_comment("Like", _("Communication: {0} in {1}").format("<b>" + doc.subject + "</b>", link),
reference_doctype=doc.reference_doctype, reference_name=doc.reference_name)
else:
doc.add_comment("Like", _("Liked"))

View file

@ -24,7 +24,9 @@ def get_notifications():
return {
"open_count_doctype": get_notifications_for_doctypes(config, notification_count),
"open_count_module": get_notifications_for_modules(config, notification_count),
"open_count_other": get_notifications_for_other(config, notification_count),
"new_messages": get_new_messages()
# "likes": get_count_of_new_likes()
}
def get_new_messages():
@ -48,19 +50,27 @@ def get_new_messages():
def get_notifications_for_modules(config, notification_count):
"""Notifications for modules"""
open_count_module = {}
for m in config.for_module:
return get_notifications_for("for_module", config, notification_count)
def get_notifications_for_other(config, notification_count):
"""Notifications for other items"""
return get_notifications_for("for_other", config, notification_count)
def get_notifications_for(notification_type, config, notification_count):
open_count = {}
notification_map = config.get(notification_type) or {}
for m in notification_map:
try:
if m in notification_count:
open_count_module[m] = notification_count[m]
open_count[m] = notification_count[m]
else:
open_count_module[m] = frappe.get_attr(config.for_module[m])()
open_count[m] = frappe.get_attr(notification_map[m])()
frappe.cache().hset("notification_count:" + m, frappe.session.user, open_count_module[m])
frappe.cache().hset("notification_count:" + m, frappe.session.user, open_count[m])
except frappe.PermissionError:
frappe.msgprint("Permission Error in notifications for {0}".format(m))
return open_count_module
return open_count
def get_notifications_for_doctypes(config, notification_count):
"""Notifications for DocTypes"""
@ -143,7 +153,7 @@ def get_notification_config():
config = frappe._dict()
for notification_config in frappe.get_hooks().notification_config:
nc = frappe.get_attr(notification_config)()
for key in ("for_doctype", "for_module", "for_module_doctypes"):
for key in ("for_doctype", "for_module", "for_module_doctypes", "for_other"):
config.setdefault(key, {})
config[key].update(nc.get(key, {}))
return config

View file

@ -62,3 +62,12 @@
width: 97% !important;
margin: auto;
}
#page-activity .list-filters {
display: none !important;
}
#page-activity .octicon-heart {
color: #ff5858;
margin: 0px 5px;
}

View file

@ -18,36 +18,73 @@ frappe.pages['activity'].on_page_load = function(wrapper) {
this.page.set_title(__("Activity"));
this.page.list = new frappe.ui.Listing({
hide_refresh: true,
page: this.page,
method: 'frappe.desk.page.activity.activity.get_feed',
parent: $("<div></div>").appendTo(this.page.main),
render_row: function(row, data) {
new frappe.activity.Feed(row, data);
}
});
frappe.model.with_doctype("Feed", function() {
me.page.list = new frappe.ui.Listing({
hide_refresh: true,
page: me.page,
method: 'frappe.desk.page.activity.activity.get_feed',
parent: $("<div></div>").appendTo(me.page.main),
render_row: function(row, data) {
new frappe.activity.Feed(row, data);
},
show_filters: true,
doctype: "Feed",
get_args: function() {
if (frappe.route_options && frappe.route_options.show_likes) {
delete frappe.route_options.show_likes;
return {
show_likes: true
}
} else {
return {}
}
}
});
this.page.list.run();
me.page.list.run();
me.page.set_primary_action(__("Refresh"), function() {
me.page.list.filter_list.clear_filters();
me.page.list.run();
}, "octicon octicon-sync");
});
frappe.activity.render_plot(this.page);
this.page.main.on("click", ".activity-message", function() {
var doctype = $(this).attr("data-doctype"),
var reference_doctype = $(this).attr("data-reference-doctype"),
reference_name = $(this).attr("data-reference-name"),
doctype = $(this).attr("data-doctype"),
docname = $(this).attr("data-docname");
if (doctype && docname) {
frappe.set_route(["Form", doctype, docname]);
frappe.set_route(["Form", reference_doctype || doctype, reference_name || docname]);
if (reference_doctype && reference_name) {
frappe.route_options = {
scroll_to: { "doctype": doctype, "name": docname }
}
}
}
});
this.page.set_primary_action(__("Refresh"), function() { me.page.list.run(); }, "octicon octicon-sync");
// Build Report Button
if(frappe.boot.user.can_get_report.indexOf("Feed")!=-1) {
this.page.set_secondary_action(__('Build Report'), function() {
this.page.add_menu_item(__('Build Report'), function() {
frappe.set_route('Report', "Feed");
}, 'icon-th');
}, 'icon-th')
}
this.page.add_menu_item(__('Show Likes'), function() {
frappe.route_options = {
show_likes: true
};
me.page.list.run();
}, 'octicon octicon-heart');
};
frappe.pages['activity'].on_page_show = function() {
frappe.breadcrumbs.add("Desk");
}
frappe.activity.last_feed_date = false;
@ -69,7 +106,7 @@ frappe.activity.Feed = Class.extend({
.find("a").addClass("grey");
},
scrub_data: function(data) {
data.by = frappe.user_info(data.owner).fullname;
data.by = frappe.user.full_name(data.owner);
data.imgsrc = frappe.utils.get_file_link(frappe.user_info(data.owner).image);
data.icon = "icon-flag";

View file

@ -3,14 +3,38 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cint
from frappe.desk.doctype.feed.feed import get_permission_query_conditions
@frappe.whitelist()
def get_feed(limit_start, limit_page_length):
def get_feed(limit_start, limit_page_length, show_likes=False):
"""get feed"""
return frappe.get_list("Feed", fields=["name", "feed_type", "doc_type",
"subject", "owner", "modified", "doc_name", "creation"],
limit_start = limit_start, limit_page_length = limit_page_length,
order_by="creation desc")
# directly use the permission query condition function of feed
match_conditions = get_permission_query_conditions(frappe.session.user)
result = frappe.db.sql("""select name, feed_type, doc_type, doc_name, subject,
owner, modified, creation, seen, reference_doctype, reference_name
from `tabFeed`
where
((feed_type='Like' and (owner=%(user)s or doc_owner=%(user)s)) or feed_type!='Like')
{match_conditions}
{show_likes}
order by creation desc
limit %(limit_start)s, %(limit_page_length)s"""
.format(match_conditions="and {0}".format(match_conditions) if match_conditions else "",
show_likes="and feed_type='Like'" if show_likes else ""),
{
"user": frappe.session.user,
"limit_start": cint(limit_start),
"limit_page_length": cint(limit_page_length)
}, as_dict=True)
if show_likes:
# mark likes as seen!
frappe.db.sql("update `tabFeed` set seen=1 where feed_type='Like' and doc_owner=%s", frappe.session.user)
frappe.local.flags.commit = True
return result
@frappe.whitelist()
def get_months_activity():

View file

@ -1,10 +1,15 @@
<div class="row activity-row" data-creation="{%= creation.split(" ")[0] + " 00:00:00" %}">
<div class="col-xs-3 text-right activity-date"><span class="{%= date_class %}">
{%= date_sep || "" %}</span></div>
<div class="col-xs-9 activity-message" data-doctype="{%= doc_type %}" data-docname="{%= doc_name %}"
<div class="col-xs-9 activity-message"
data-doctype="{%= doc_type %}"
data-docname="{%= doc_name %}"
data-reference-doctype="{{ reference_doctype }}"
data-reference-name="{{ reference_name }}"
title="{%= by %} / {%= dateutil.str_to_user(creation) %}">
<span class="avatar avatar-small">
<img src="{%= imgsrc %}">
<div class="avatar-frame" style="background-image: url({{ imgsrc }});"></div>
<!-- <img src="{%= imgsrc %}"> -->
</span>
<span class="small">
{% if (feed_type==="Login") { %}
@ -15,6 +20,13 @@
{%= __("Commented on {0}: {1}", [link, "<strong>" + subject + "</strong>"]) %}
{% } else if (doc_type && !feed_type) { %}
{%= __("Updated {0}: {1}", [link, "<strong>" + subject + "</strong>"]) %}
{% } else if (feed_type==="Like" && doc_type) { %}
{%= by %} <i class="octicon octicon-heart"></i>
{% if (in_list(["Comment", "Communication"], doc_type)) { %}
{%= subject %}
{% } else { %}
{%= link %}
{% } %}
{% } else if (doc_type) { %}
{%= __("{0}: {1}", [link, "<strong>" + subject + "</strong>"]) %}
{% } else { %}

View file

@ -22,6 +22,8 @@ frappe.pages.messages.on_page_load = function(parent) {
frappe.pages.messages.on_page_show = function() {
// clear title prefix
frappe.utils.set_title_prefix("");
frappe.breadcrumbs.add("Desk");
}
frappe.desk.pages.Messages = Class.extend({

View file

@ -1,53 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
"""Allow adding of stars to documents"""
import frappe, json
from frappe.model.db_schema import add_column
@frappe.whitelist()
def toggle_star(doctype, name, add=False):
"""Adds / removes the current user in the `__starred_by` property of the given document.
If column does not exist, will add it in the database.
The `_starred_by` property is always set from this function and is ignored if set via
Document API
:param doctype: DocType of the document to star
:param name: Name of the document to star
:param add: `Yes` if star is to be added. If not `Yes` the star will be removed."""
_toggle_star(doctype, name, add)
def _toggle_star(doctype, name, add=False, user=None):
"""Same as toggle_star but hides param `user` from API"""
if not user:
user = frappe.session.user
try:
starred_by = frappe.db.get_value(doctype, name, "_starred_by")
if starred_by:
starred_by = json.loads(starred_by)
else:
starred_by = []
if add=="Yes":
if user not in starred_by:
starred_by.append(user)
else:
if user in starred_by:
starred_by.remove(user)
frappe.db.sql("""update `tab{0}` set `_starred_by`=%s where name=%s""".format(doctype),
(json.dumps(starred_by), name))
except Exception, e:
if e.args[0]==1054:
add_column(doctype, "_starred_by", "Text")
_toggle_star(doctype, name, add, user)
else:
raise

View file

@ -1,64 +0,0 @@
<!-- title: frappe.desk.star --><div class="dev-header">
<a class="btn btn-default btn-sm" disabled style="margin-bottom: 10px;">
Version 6.x.x</a>
<a class="btn btn-default btn-sm" href="https://github.com/frappe/frappe/blob/develop/frappe/desk/star.py"
target="_blank" style="margin-left: 10px; margin-bottom: 10px;"><i class="octicon octicon-mark-github"></i> Source</a>
</div>
<p class="docs-attr-name">
<a name="frappe.desk.star._toggle_star" href="#frappe.desk.star._toggle_star" class="text-muted small">
<i class="icon-link small" style="color: #ccc;"></i></a>
frappe.desk.star.<b>_toggle_star</b>
<i class="text-muted">(doctype, name, add=False, user=None)</i>
</p>
<div class="docs-attr-desc"><p>Same as toggle_star but hides param <code>user</code> from API</p>
</div>
<br>
<p><span class="label label-info">Public API</span>
<br><code>/api/method/frappe.desk.star.toggle_star</code>
</p>
<p class="docs-attr-name">
<a name="frappe.desk.star.toggle_star" href="#frappe.desk.star.toggle_star" class="text-muted small">
<i class="icon-link small" style="color: #ccc;"></i></a>
frappe.desk.star.<b>toggle_star</b>
<i class="text-muted">(doctype, name, add=False)</i>
</p>
<div class="docs-attr-desc"><p>Adds / removes the current user in the <code>__starred_by</code> property of the given document.
If column does not exist, will add it in the database.</p>
<p>The <code>_starred_by</code> property is always set from this function and is ignored if set via
Document API</p>
<p><strong>Parameters:</strong></p>
<ul>
<li><strong><code>doctype</code></strong> - DocType of the document to star</li>
<li><strong><code>name</code></strong> - Name of the document to star</li>
<li><strong><code>add</code></strong> - <code>Yes</code> if star is to be added. If not <code>Yes</code> the star will be removed.</li>
</ul>
</div>
<br>
<!-- autodoc -->

View file

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
from frappe.utils.pdf import get_pdf
from frappe.email.smtp import get_outgoing_email_account
from frappe.utils import get_url, scrub_urls, strip, expand_relative_urls, cint, split_emails
from frappe.utils import get_url, scrub_urls, strip, expand_relative_urls, cint, split_emails, to_markdown
import email.utils
from markdown2 import markdown
@ -89,12 +89,7 @@ class EMail:
def set_html_as_text(self, html):
"""return html2text"""
import HTMLParser
from html2text import html2text
try:
self.set_text(html2text(html))
except HTMLParser.HTMLParseError:
pass
self.set_text(to_markdown(html))
def set_message(self, message, mime_type='text/html', as_attachment=0, filename='attachment.html'):
"""Append the message with MIME content to the root node (as attachment)"""

View file

@ -14,7 +14,7 @@ default_fields = ('doctype','name','owner','creation','modified','modified_by',
integer_docfield_properties = ("reqd", "search_index", "in_list_view", "permlevel",
"hidden", "read_only", "ignore_user_permissions", "allow_on_submit", "report_hide",
"in_filter", "no_copy", "print_hide", "unique")
optional_fields = ("_user_tags", "_comments", "_assign", "_starred_by")
optional_fields = ("_user_tags", "_comments", "_assign", "_liked_by")
def rename(doctype, old, new, debug=False):
import frappe.model.rename_doc

View file

@ -239,7 +239,7 @@ class BaseDocument(object):
if k in default_fields:
del doc[k]
for key in ("_user_tags", "__islocal", "__onload", "_starred_by", "__run_link_triggers"):
for key in ("_user_tags", "__islocal", "__onload", "_liked_by", "__run_link_triggers"):
if self.get(key):
doc[key] = self.get(key)

View file

@ -25,7 +25,7 @@ class DatabaseQuery(object):
def execute(self, query=None, fields=None, filters=None, or_filters=None,
docstatus=None, group_by=None, order_by=None, limit_start=False,
limit_page_length=None, as_list=False, with_childnames=False, debug=False,
ignore_permissions=False, user=None):
ignore_permissions=False, user=None, with_comment_count=False):
if not ignore_permissions and not frappe.has_permission(self.doctype, "read", user=user):
raise frappe.PermissionError, self.doctype
@ -45,9 +45,14 @@ class DatabaseQuery(object):
self.user = user or frappe.session.user
if query:
return self.run_custom_query(query)
result = self.run_custom_query(query)
else:
return self.build_and_run()
result = self.build_and_run()
if with_comment_count and not as_list and self.doctype:
self.add_comment_count(result)
return result
def build_and_run(self):
args = self.prepare_args()
@ -444,3 +449,21 @@ class DatabaseQuery(object):
return 'limit %s, %s' % (self.limit_start, self.limit_page_length)
else:
return ''
def add_comment_count(self, result):
for r in result:
if not r.name:
continue
if "_comments" in r:
comment_count = len(json.loads(r._comments or "[]"))
else:
comment_count = cint(frappe.db.get_value("Comment",
filters={"comment_doctype": self.doctype, "comment_docname": r.name, "comment_type": "Comment"},
fieldname="count(name)"))
communication_count = cint(frappe.db.get_value("Communication",
filters={"reference_doctype": self.doctype, "reference_name": r.name},
fieldname="count(name)"))
r._comment_count = comment_count + communication_count

View file

@ -764,7 +764,7 @@ class Document(BaseDocument):
"""Returns Desk URL for this document. `/desk#Form/{doctype}/{name}`"""
return "/desk#Form/{doctype}/{name}".format(doctype=self.doctype, name=self.name)
def add_comment(self, comment_type, text=None, comment_by=None):
def add_comment(self, comment_type, text=None, comment_by=None, reference_doctype=None, reference_name=None):
"""Add a comment to this document.
:param comment_type: e.g. `Comment`. See Comment for more info."""
@ -774,7 +774,9 @@ class Document(BaseDocument):
"comment_type": comment_type,
"comment_doctype": self.doctype,
"comment_docname": self.name,
"comment": text or _(comment_type)
"comment": text or _(comment_type),
"reference_doctype": reference_doctype,
"reference_name": reference_name
}).insert(ignore_permissions=True)
return comment
@ -782,10 +784,10 @@ class Document(BaseDocument):
"""Returns signature (hash) for private URL."""
return hashlib.sha224(get_datetime_str(self.creation)).hexdigest()
def get_starred_by(self):
starred_by = getattr(self, "_starred_by", None)
if starred_by:
return json.loads(starred_by)
def get_liked_by(self):
liked_by = getattr(self, "_liked_by", None)
if liked_by:
return json.loads(liked_by)
else:
return []

View file

@ -75,6 +75,7 @@ execute:frappe.db.sql("""update tabComment set comment = substr(comment, 6, loca
frappe.patches.v5_0.fix_feed
frappe.patches.v5_0.update_shared
execute:frappe.reload_doc("core", "doctype", "docshare") #2015-07-21
frappe.patches.v6_16.star_to_like
frappe.patches.v5_0.bookmarks_to_stars
frappe.patches.v5_0.style_settings_to_website_theme
frappe.patches.v5_0.rename_ref_type_fieldnames
@ -112,3 +113,5 @@ execute:frappe.create_folder(os.path.join(frappe.local.site_path, 'private', 'fi
frappe.patches.v6_15.remove_property_setter_for_previous_field #2015-12-29
frappe.patches.v6_15.set_username
execute:frappe.permissions.reset_perms("Error Snapshot")
frappe.patches.v6_16.feed_doc_owner

View file

@ -2,7 +2,7 @@ from __future__ import unicode_literals
import json
import frappe
import frappe.defaults
from frappe.desk.star import _toggle_star
from frappe.desk.like import _toggle_like
def execute():
for user in frappe.get_all("User"):
@ -29,4 +29,4 @@ def execute():
or int(frappe.db.get_value("DocType", doctype, "issingle") or 0)
or not frappe.db.table_exists(doctype)):
continue
_toggle_star(doctype, docname, add="Yes", user=username)
_toggle_like(doctype, docname, add="Yes", user=username)

View file

View file

@ -0,0 +1,30 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doctype("Feed")
frappe.db.sql("update `tabFeed` set seen=1")
for doctype, name in frappe.db.sql("""select distinct doc_type, doc_name from `tabFeed`
where
(doc_type is not null and doc_type != '')
and (doc_name is not null and doc_name != '')
and doc_type != 'Feed'
for update"""):
owner = frappe.db.get_value(doctype, name, "owner")
if not owner:
continue
frappe.db.sql("""update `tabFeed`
set doc_owner=%(owner)s
where
doc_type=%(doctype)s
and doc_name=%(name)s
and (doc_owner is null or doc_owner = '')""".format(doctype=doctype), {
"doctype": doctype,
"name": name,
"owner": owner
})

View file

@ -0,0 +1,18 @@
from __future__ import unicode_literals
import frappe
from frappe.model.db_schema import add_column
def execute():
frappe.reload_doctype("Feed")
frappe.db.sql("""update `tabSingles` set field='_liked_by' where field='_starred_by'""")
frappe.db.commit()
for table in frappe.db.get_tables():
columns = [r[0] for r in frappe.db.sql("DESC `{0}`".format(table))]
if "_starred_by" in columns:
frappe.db.sql_ddl("""alter table `{0}` change `_starred_by` `_liked_by` Text """.format(table))
for doctype in ("Comment", "Communication"):
if not frappe.db.has_column(doctype, "_liked_by"):
add_column(doctype, "_liked_by", "Text")

View file

@ -16,6 +16,7 @@
"public/js/lib/highlight.pack.js",
"public/js/frappe/class.js",
"public/js/lib/microtemplate.js",
"public/js/frappe/query_string.js",
"website/js/website.js",
"public/js/lib/socket.io.min.js"
],
@ -121,6 +122,7 @@
"public/js/frappe/change_log.html",
"public/js/frappe/desk.js",
"public/js/frappe/query_string.js",
"public/html/error_object.html",
"public/html/error_snapshot.html"
@ -180,7 +182,8 @@
"public/js/frappe/ui/filters/filters.js",
"public/js/frappe/ui/filters/edit_filter.html",
"public/js/frappe/ui/tags.js",
"public/js/frappe/ui/star.js",
"public/js/frappe/ui/like.js",
"public/js/frappe/ui/liked_by.html",
"public/html/print_template.html",
"public/js/frappe/list/doclistview.js",
"public/js/frappe/list/list_sidebar.js",

View file

@ -43,3 +43,12 @@
height: 0;
padding-bottom: 100%;
}
.avatar-frame {
width: 100%;
height: 0;
padding: 50% 0px;
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
border-radius: 4px;
}

View file

@ -546,3 +546,11 @@ ul.linked-with-list li {
border-bottom: 1px solid #d1d8dd;
border-radius: 0px;
}
.liked-by-popover {
min-width: 200px;
margin-top: -10px;
margin-bottom: -10px;
}
.liked-by-popover li {
margin: 15px 0px;
}

View file

@ -61,8 +61,7 @@
.empty-section {
display: none !important;
}
.shaded-section,
.timeline-item:nth-child(even) {
.shaded-section {
background-color: #fafbfc;
}
.modal .form-layout {
@ -87,37 +86,165 @@
background-color: #e74c3c;
}
.timeline {
border: 1px solid #d1d8dd;
margin: 30px 0px;
}
.timeline .timeline-head .comment-input {
height: auto;
}
.timeline-item {
margin-top: 0px;
padding: 15px 30px 7px;
border-bottom: 1px solid #d1d8dd;
}
.timeline-item .icon-fixed-width {
text-align: center;
}
.timeline-item blockquote {
font-size: inherit;
}
.timeline-item:last-child {
border-bottom: 0px;
.timeline-items {
position: relative;
}
.timeline-item .reply {
margin-top: 5px;
.timeline {
position: relative;
}
.timeline::before {
content: " ";
border-left: 1px solid #d1d8dd;
position: absolute;
top: 0px;
bottom: -124px;
left: 43px;
z-index: -1;
}
@media (max-width: 991px) {
.timeline::before {
bottom: -64px;
}
}
.timeline-item.user-content {
margin: 30px 0px 30px 30px;
}
.timeline-item.user-content .media-body {
border: 1px solid #d1d8dd;
border-radius: 2px;
margin-left: -7px;
position: relative;
overflow: visible;
}
.timeline-item.user-content .comment-header {
background-color: #fafbfc;
padding: 7px 15px;
margin: 0px;
color: #8D99A6;
border-bottom: 1px solid #EBEFF2;
}
.timeline-item.user-content .comment-header .octicon-heart {
color: #ff5858;
cursor: pointer;
}
.timeline-item.user-content .reply {
padding: 10px 15px;
}
.timeline-item.user-content .reply > div > p:first-child {
margin-top: 0px;
}
.timeline-item.user-content .reply > div > p:last-child {
margin-bottom: 0px;
}
.timeline-item.user-content .close-btn-container {
padding: 2px 15px;
}
.timeline-item.user-content .comment-likes {
margin-left: 5px;
}
.timeline-item.user-content .media-body:after,
.timeline-item.user-content .media-body:before {
right: 100%;
top: 15px;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
.timeline-item.user-content .media-body:after {
border-color: rgba(136, 183, 213, 0);
border-right-color: #fafbfc;
border-width: 6px;
margin-top: -6px;
}
.timeline-item.user-content .media-body:before {
border-color: rgba(194, 225, 245, 0);
border-right-color: #d1d8dd;
border-width: 7px;
margin-top: -7px;
}
.timeline-item.notification-content {
padding-left: 30px;
margin: 30px 0px;
position: relative;
color: #8D99A6;
}
.timeline-item.notification-content * {
color: #8D99A6;
}
.timeline-item.notification-content .icon-fixed-width {
margin-left: 36px;
}
.timeline-item.notification-content .octicon-heart {
color: #ff5858 !important;
}
.timeline-item.notification-content::before {
content: " ";
width: 7px;
height: 7px;
background-color: #d1d8dd;
position: absolute;
left: 40px;
border-radius: 50%;
top: 5px;
}
.timeline-item .reply-link {
padding: 0px 7px;
}
.timeline-item h6,
.timeline-head h6 {
margin-top: 6px;
padding-left: 7px;
}
.timeline-head {
background-color: white;
border: 1px solid #d1d8dd;
border-radius: 2px;
}
.timeline-head .comment-input-header {
background-color: #fafbfc;
padding: 15px 30px;
border-bottom: 1px solid #d1d8dd;
padding: 7px 15px;
border-bottom: 1px solid #EBEFF2;
}
.timeline-head .comment-input-container {
padding: 15px;
}
.timeline-head .comment-input {
border-color: #EBEFF2;
max-width: 100%;
}
.timeline-head .comment-input:focus {
box-shadow: none;
}
@media (max-width: 767px) {
.timeline-head {
border-left: none;
border-right: none;
border-radius: 0px;
}
}
.timeline-new-email {
margin: 30px 0px;
padding: 0px 65px;
position: relative;
}
.timeline-new-email::before {
content: " ";
width: 7px;
height: 7px;
background-color: #d1d8dd;
position: absolute;
left: 40px;
border-radius: 50%;
top: 5px;
}
.form-footer h5 {
margin: 15px 0px;

View file

@ -48,3 +48,11 @@
.indicator-right.darkgrey::after {
background: #b8c2cc;
}
.indicator.yellow::before,
.indicator-right.yellow::after {
background: #FEEF72;
}
.indicator.light-blue::before,
.indicator-right.light-blue::after {
background: #7CD6FD;
}

View file

@ -98,6 +98,13 @@
.doclist-row {
font-size: 12px;
}
.doclist-row .likes-count {
display: inline-block;
width: 15px;
margin-left: -5px;
color: #8D99A6;
font-size: 12px;
}
.doclist-row .docstatus .octicon {
font-size: 12px;
}
@ -134,14 +141,14 @@
margin: 0px -15px;
padding: 5px 15px;
}
.listview-main-section .icon-star {
.listview-main-section .octicon-heart {
cursor: pointer;
}
.list-row-head .icon-star {
vertical-align: middle;
.list-row-head .octicon-heart {
margin-right: 13px;
}
.star-action.icon-star {
color: #ffdb4c;
.like-action.octicon-heart {
color: #ff5858;
}
.list-id {
font-weight: bold;
@ -163,3 +170,8 @@
display: none;
}
}
.list-comment-count {
display: inline-block;
width: 37px;
text-align: left;
}

View file

@ -330,20 +330,6 @@ body {
border-left-color: transparent !important;
border-right-color: transparent !important;
}
.timeline {
border-left: none !important;
border-right: none !important;
}
.timeline .timeline-head {
padding: 7px 15px;
}
.timeline .timeline-item {
padding: 15px;
border-bottom: 1px dashed #d1d8dd;
}
.timeline .timeline-item:last-child {
border-bottom: none;
}
.list-row {
padding: 13px 15px !important;
}

View file

@ -161,6 +161,10 @@ body[data-route^="Module"] .main-menu .form-sidebar {
.form-sidebar .form-viewers .shared-with-everyone .octicon {
color: #36414C !important;
}
.form-sidebar .liked-by .octicon-heart {
font-size: 16px;
cursor: pointer;
}
.form-sidebar .form-shared .share-doc-btn:hover,
.form-sidebar .form-shared .share-doc-btn:focus,
.form-sidebar .form-shared .share-doc-btn:active {
@ -174,6 +178,7 @@ body[data-route^="Module"] .main-menu .form-sidebar {
.sidebar-left .form-sidebar .form-tags,
.sidebar-left .form-sidebar .assignment-row,
.sidebar-left .form-sidebar .form-shared,
.sidebar-left .form-sidebar .liked-by,
.sidebar-left .form-sidebar .modified-by,
.sidebar-left .form-sidebar .created-by,
.sidebar-left .form-sidebar .tags-label,

View file

@ -264,6 +264,16 @@ a.no-decoration:active {
height: 0;
padding-bottom: 100%;
}
.avatar-frame {
width: 100%;
height: 0;
padding: 50% 0px;
background-size: cover;
background-repeat: no-repeat;
background-position: center top;
border-radius: 0.5em;
border: 1px solid #EBEFF2;
}
.indicator,
.indicator-right {
background: none;
@ -314,6 +324,14 @@ a.no-decoration:active {
.indicator-right.darkgrey::after {
background: #b8c2cc;
}
.indicator.yellow::before,
.indicator-right.yellow::after {
background: #FEEF72;
}
.indicator.light-blue::before,
.indicator-right.light-blue::after {
background: #7CD6FD;
}
.navbar-brand {
max-width: none;
}

View file

@ -1,37 +1,21 @@
<div class="timeline">
<div class="timeline-head">
<h6 class="text-muted">{%= __("Add a comment") %}</h6>
<div>
<textarea style="height: 80px" style="margin-top: 10px;"
class="form-control"></textarea>
<div class="comment-input-header">
<span class="small text-muted">{%= __("Add a comment") %}</span>
<button class="btn btn-default btn-comment btn-xs pull-right">
{%= __("Comment") %}
</button>
</div>
<div class="comment-input-container">
<textarea class="form-control comment-input"></textarea>
<input type="data" class="hidden mention-input">
</div>
<div class="media">
<span class="pull-left avatar avatar-medium">
<img class="media-object" src="{%= image %}">
</span>
<div class="media-body">
<div class="row">
<div class="col-xs-4">
<h6>{%= __("You") %}</h6>
</div>
<div class="col-xs-8 text-right" style="margin-top: 2px;">
<button class="btn btn-primary btn-go btn-xs pull-right">
{%= __("Comment") %}
</button>
<div class="checkbox text-muted small pull-right"
style="margin-top: 3px; margin-right: 15px;">
<label>
<input type="checkbox" class="is-email" style="margin-top: 1px;">
{%= __("Email") %}
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="timeline-new-email">
<button class="btn btn-default btn-new-email btn-xs">
{%= __("New Email") %}
</button>
</div>
<div class="timeline-items">
</div>

View file

@ -14,33 +14,31 @@ frappe.ui.form.Comments = Class.extend({
this.list = this.wrapper.find(".timeline-items");
this.input = this.wrapper.find(".form-control");
this.button = this.wrapper.find(".btn-go")
this.comment_button = this.wrapper.find(".btn-comment")
.on("click", function() {
if(me.wrapper.find(".is-email").prop("checked")) {
new frappe.views.CommunicationComposer({
doc: me.frm.doc,
txt: frappe.markdown(me.input.val()),
frm: me.frm,
recipients: me.get_recipient()
})
} else {
me.add_comment(this);
}
me.add_comment(this);
});
this.input.keydown("meta+return ctrl+return", function(e) {
me.button.trigger("click");
me.comment_button.trigger("click");
});
this.email_check = this.wrapper.find(".timeline-head input[type='checkbox']")
.on("change", function() {
me.button.html($(this).prop("checked") ? __("Compose") : __("Comment"));
this.email_button = this.wrapper.find(".btn-new-email")
.on("click", function() {
new frappe.views.CommunicationComposer({
doc: me.frm.doc,
txt: frappe.markdown(me.input.val()),
frm: me.frm,
recipients: me.get_recipient()
})
});
this.list.on("click", ".toggle-blockquote", function() {
$(this).parent().siblings("blockquote").toggleClass("hidden");
});
this.setup_comment_like();
this.setup_mentions();
},
@ -116,7 +114,7 @@ frappe.ui.form.Comments = Class.extend({
prepare_comment: function(c) {
if((c.comment_type || "Comment") === "Comment" && frappe.model.can_delete("Comment")) {
c["delete"] = '<a class="close" href="#">&times;</a>';
c["delete"] = '<a class="close" href="#"><i class="octicon octicon-trashcan"></i></a>';
} else {
c["delete"] = "";
}
@ -130,7 +128,7 @@ frappe.ui.form.Comments = Class.extend({
c.image = frappe.user_info(c.comment_by).image
|| frappe.get_gravatar(c.comment_by);
c.comment_on = comment_when(c.creation);
c.fullname = frappe.user_info(c.comment_by).fullname;
c.fullname = frappe.user.full_name(c.comment_by);
if(c.attachments && typeof c.attachments==="string")
c.attachments = JSON.parse(c.attachments);
@ -167,6 +165,15 @@ frappe.ui.form.Comments = Class.extend({
if(c.comment_type==="Comment") {
c.comment_html = c.comment_html.replace(/(^|\W)(@\w+)/g, "$1<b>$2</b>");
}
if (in_list(["Comment", "Email"], c.comment_type)) {
c.user_content = true;
if (!$.isArray(c._liked_by)) {
c._liked_by = JSON.parse(c._liked_by || "[]");
}
c.liked_by_user = c._liked_by.indexOf(user)!==-1;
}
}
},
set_icon_and_color: function(c) {
@ -186,7 +193,8 @@ frappe.ui.form.Comments = Class.extend({
"Attachment": "octicon octicon-cloud-upload",
"Attachment Removed": "octicon octicon-trashcan",
"Shared": "octicon octicon-eye",
"Unshared": "octicon octicon-circle-slash"
"Unshared": "octicon octicon-circle-slash",
"Like": "octicon octicon-heart"
}[c.comment_type]
c.color = {
@ -348,10 +356,13 @@ frappe.ui.form.Comments = Class.extend({
this.mention_input = this.wrapper.find(".mention-input");
var source = Object.keys(username_user_map);
source.sort();
this.mention_input.autocomplete({
minLength: 0,
autoFocus: true,
source: Object.keys(username_user_map),
source: source,
select: function(event, ui) {
var value = ui.item.value;
var textarea_value = me.input.val();
@ -467,9 +478,21 @@ frappe.ui.form.Comments = Class.extend({
me.mention_widget.trigger(me.enter);
// prevent default
return false;
}
} else {
if (e.which==me.codes.TAB) {
me.comment_button.focus();
return false;
}
}
});
},
setup_comment_like: function() {
this.wrapper.on("click", ".comment-likes .octicon-heart", frappe.ui.click_toggle_like);
frappe.ui.setup_like_popover(this.wrapper, ".comment-likes");
}
});

View file

@ -1,16 +1,19 @@
<div class="media timeline-item" data-name="{%= data.name %}">
<div class="media timeline-item {% if (data.user_content) { %} user-content {% } else { %} notification-content {% } %}" data-doctype="{{ data.doctype }}" data-name="{%= data.name %}">
{% if (data.user_content) { %}
<span class="pull-left avatar avatar-medium">
<img class="media-object" src="{%= data.image %}">
<div class="avatar-frame" style="background-image: url({%= data.image %})"></div>
</span>
<div class="pull-left media-body" style="max-width: calc(100% - 41px); padding-right: 0px;">
<div>
<div class="pull-right">
{% } %}
<div class="pull-left media-body" style="max-width: calc(100% - 50px); padding-right: 0px;">
<div class="media-content-wrapper">
<div class="pull-right close-btn-container">
<span class="small text-muted">
{%= data.delete %}
</span>
</div>
{% if(data.doctype=="Communication" || data.comment_type=="Comment") { %}
<h6>
<div class="comment-header small">
<i class="{%= data.icon %} icon-fixed-width"></i>
<span title="{%= data.comment_by %}">{%= data.fullname %}</span>
<span class="text-muted" style="font-weight: normal;">
@ -47,14 +50,23 @@
<a class="text-muted reply-link pull-right"
data-name="{%= data.name %}">{%= __("Reply") %}</a>
{% } %}
</h6>
<span class="comment-likes" data-liked-by=\'{{ JSON.stringify(data._liked_by) }}\'>
<i class="octicon octicon-heart like-action
{% if (!data.liked_by_user) { %}
text-extra-muted not-liked
{% } %} "
data-doctype="{%= data.doctype %}"
data-name="{%= data.name %}"></i>
<span class="likes-count text-muted">{{ data._liked_by.length }}</span>
</span>
</div>
<div class="reply">
<div>
{%= data.comment_html %}
</div>
</div>
{% } else if(in_list(["Assignment Completed", "Assigned", "Shared", "Unshared"], data.comment_type)) { %}
<h6>
<div class="small">
<i class="{%= data.icon %} icon-fixed-width"></i>
{% if(data.reference_doctype && data.reference_name) { %}
<a href="#Form/{%= data.reference_doctype %}/{%= data.reference_name %}">
@ -65,14 +77,19 @@
{% } %}
<span class="text-muted" style="font-weight: normal;">
&ndash; {%= data.comment_on %}</span>
</h6>
</div>
{% } else { %}
<h6>
<div class="small">
<i class="{%= data.icon %} icon-fixed-width"></i>
<span title="{%= data.comment_by %}">{%= data.fullname %}</span> {%= data.comment %}
<span class="text-muted" style="font-weight: normal;">
&ndash; {%= data.comment_on %}</span>
</h6>
{% if (data.comment_type == "Like") { %}
<span title="{%= data.comment_by %}">{%= __("Liked by {0}", [data.fullname]) %}</span>
{% } else { %}
<span title="{%= data.comment_by %}">{%= data.fullname %}</span>
{%= data.comment %}
{% } %}
<span class="text-muted" style="font-weight: normal;">
&ndash; {%= data.comment_on %}</span>
</div>
{% } %}
{% if(data.attachments && data.attachments.length) { %}
<div style="margin: 10px 0px">

View file

@ -45,6 +45,12 @@
<li class="form-viewers"></li>
</ul>
<ul class="list-unstyled sidebar-menu text-muted">
<li class="liked-by-parent">
<span class="liked-by">
<i class="octicon octicon-heart like-action text-extra-muted"></i>
<span class="like-count"></span>
</span>
</li>
<li class="modified-by"></li>
<li class="created-by"></li>
</ul>

View file

@ -117,7 +117,7 @@ frappe.form.formatters = {
return frappe.form.formatters.Data(value);
},
StarredBy: function(value) {
LikedBy: function(value) {
var html = "";
$.each(JSON.parse(value || "[]"), function(i, v) {
if(v) html+= '<span class="avatar avatar-small" \

View file

@ -22,6 +22,7 @@ frappe.ui.form.Sidebar = Class.extend({
this.make_shared();
this.make_viewers();
this.make_tags();
this.make_like();
this.bind_events();
@ -37,6 +38,12 @@ frappe.ui.form.Sidebar = Class.extend({
$(".offcanvas").removeClass("active-left active-right");
frappe.ui.scroll(me.frm.footer.wrapper.find(".form-comments"), true);
});
this.like_icon.on("click", function() {
frappe.ui.toggle_like(me.like_icon, me.frm.doctype, me.frm.doc.name, function() {
me.refresh_like();
});
})
},
refresh: function() {
@ -55,6 +62,8 @@ frappe.ui.form.Sidebar = Class.extend({
this.sidebar.find(".created-by").html(__("{0} created this {1}",
["<strong>" + frappe.user.full_name(this.frm.doc.owner) + "</strong>",
"<br>" + comment_when(this.frm.doc.creation)]));
this.refresh_like();
}
},
@ -113,4 +122,26 @@ frappe.ui.form.Sidebar = Class.extend({
this.user_actions.addClass("hide")
this.user_actions.find(".user-action-row").remove();
},
make_like: function() {
this.like_wrapper = this.sidebar.find(".liked-by");
this.like_icon = this.sidebar.find(".liked-by .octicon-heart");
this.like_count = this.sidebar.find(".liked-by .like-count");
frappe.ui.setup_like_popover(this.sidebar.find(".liked-by-parent"), ".liked-by");
},
refresh_like: function() {
if (!this.like_icon) {
return;
}
this.like_wrapper.attr("data-liked-by", this.frm.doc._liked_by);
this.like_icon.toggleClass("text-extra-muted not-liked",
!frappe.ui.is_liked(this.frm.doc))
.attr("data-doctype", this.frm.doctype)
.attr("data-name", this.frm.doc.name);
this.like_count.text(JSON.parse(this.frm.doc._liked_by || "[]").length);
},
});

View file

@ -15,7 +15,6 @@ frappe.ui.form.Toolbar = Class.extend({
this.page.clear_user_actions();
this.show_title_as_dirty();
this.set_primary_action();
this.refresh_star();
if(this.frm.meta.hide_toolbar) {
this.page.hide_menu();
@ -23,11 +22,9 @@ frappe.ui.form.Toolbar = Class.extend({
if(this.frm.doc.__islocal) {
this.page.hide_menu();
this.print_icon && this.print_icon.addClass("hide");
this.star_icon && this.star_icon.addClass("hide");
} else {
this.page.show_menu();
this.print_icon && this.print_icon.removeClass("hide");
this.star_icon && this.star_icon.removeClass("hide");
}
}
},
@ -97,13 +94,6 @@ frappe.ui.form.Toolbar = Class.extend({
this.page.clear_indicator();
}
},
refresh_star: function() {
this.star_icon &&
this.star_icon.toggleClass("text-extra-muted not-starred",
!frappe.ui.is_starred(this.frm.doc))
.attr("data-doctype", this.frm.doctype)
.attr("data-name", this.frm.doc.name);
},
make_menu: function() {
var me = this;
var p = this.frm.perm[0];
@ -117,13 +107,6 @@ frappe.ui.form.Toolbar = Class.extend({
me.frm.print_doc();});
}
// star
if(!this.frm.meta.issingle) {
this.star_icon = this.page.add_action_icon("icon-star", function() {
frappe.ui.toggle_star(me.star_icon, me.frm.doctype, me.frm.doc.name);
}).removeClass("text-muted").find(".icon-star").addClass("star-action");
}
// email
if(frappe.model.can_email(null, me.frm) && me.frm.doc.docstatus < 2) {
this.page.add_menu_item(__("Email"), function() {

View file

@ -116,7 +116,7 @@ frappe.views.DocListView = frappe.ui.Listing.extend({
this.setup_filterable();
this.init_filters();
this.init_headers();
this.init_star();
this.init_like();
this.init_select_all();
},
@ -168,9 +168,9 @@ frappe.views.DocListView = frappe.ui.Listing.extend({
}
});
this.$page.find(".result-list").on("click", ".list-row-left", function(e) {
// don't open in case of checkbox, star, filterable
// don't open in case of checkbox, like, filterable
if ((e.target.className || "").indexOf("filterable")!==-1
|| (e.target.className || "").indexOf("icon-star")!==-1
|| (e.target.className || "").indexOf("octicon-heart")!==-1
|| e.target.type==="checkbox") {
return;
}
@ -307,8 +307,8 @@ frappe.views.DocListView = frappe.ui.Listing.extend({
}
}
this.list_header.find(".list-starred-by-me")
.toggleClass("text-extra-muted not-starred", !this.is_star_filtered());
this.list_header.find(".list-liked-by-me")
.toggleClass("text-extra-muted not-liked", !this.is_star_filtered());
this.last_updated_on = new Date();
this.dirty = false;
@ -355,6 +355,7 @@ frappe.views.DocListView = frappe.ui.Listing.extend({
filters: this.filter_list.get_filters(),
order_by: this.listview.order_by || undefined,
group_by: this.listview.group_by || undefined,
with_comment_count: true
}
// apply default filters, if specified for a listing
@ -368,15 +369,15 @@ frappe.views.DocListView = frappe.ui.Listing.extend({
this.filter_list.add_filter(this.doctype, "_assign", 'like', '%' + user + '%');
this.run();
},
starred_by_me: function() {
this.filter_list.add_filter(this.doctype, "_starred_by", 'like', '%' + user + '%');
liked_by_me: function() {
this.filter_list.add_filter(this.doctype, "_liked_by", 'like', '%' + user + '%');
this.run();
},
remove_starred_by_me: function() {
this.filter_list.get_filter("_starred_by").remove();
remove_liked_by_me: function() {
this.filter_list.get_filter("_liked_by").remove();
},
is_star_filtered: function() {
return this.filter_list.filter_exists(this.doctype, "_starred_by", 'like', '%' + user + '%');
return this.filter_list.filter_exists(this.doctype, "_liked_by", 'like', '%' + user + '%');
},
init_menu: function() {
var me = this;
@ -453,19 +454,20 @@ frappe.views.DocListView = frappe.ui.Listing.extend({
}, true)
},
init_star: function() {
init_like: function() {
var me = this;
this.$page.find(".result-list").on("click", ".star-action", function() {
frappe.ui.toggle_star($(this), me.doctype, $(this).attr("data-name"));
return false;
});
this.list_header.find(".list-starred-by-me").on("click", function() {
this.$page.find(".result-list").on("click", ".like-action", frappe.ui.click_toggle_like);
this.list_header.find(".list-liked-by-me").on("click", function() {
if (me.is_star_filtered()) {
me.remove_starred_by_me();
me.remove_liked_by_me();
} else {
me.starred_by_me();
me.liked_by_me();
}
});
if (!frappe.dom.is_touchscreen()) {
frappe.ui.setup_like_popover(this.$page.find(".result-list"), ".liked-by");
}
},
init_select_all: function() {
@ -576,7 +578,7 @@ frappe.views.DocListView = frappe.ui.Listing.extend({
return false;
} else {
// second filter set for this field
if(fieldname=='_user_tags' || fieldname=="_starred_by") {
if(fieldname=='_user_tags' || fieldname=="_liked_by") {
// and for tags
this.filter_list.add_filter(this.doctype, fieldname, 'like', '%' + label);
} else {
@ -587,7 +589,7 @@ frappe.views.DocListView = frappe.ui.Listing.extend({
} else {
// no filter for this item,
// setup one
if(fieldname=='_user_tags' || fieldname=="_starred_by") {
if(fieldname=='_user_tags' || fieldname=="_liked_by") {
this.filter_list.add_filter(this.doctype, fieldname, 'like', '%' + label);
} else {
this.filter_list.add_filter(this.doctype, fieldname, '=', label);

View file

@ -23,8 +23,8 @@
title="{%= __("Select All") %}">
{% } %}
<i class="icon-fixed-width icon-star text-extra-muted not-starred star-action list-starred-by-me"
title="{%= __("Starred By Me") %}"></i>
<i class="icon-fixed-width octicon octicon-heart text-extra-muted not-liked like-action list-liked-by-me"
title="{%= __("Likes") %}"></i>
{% } %}

View file

@ -33,9 +33,10 @@
{% } else { %}
<span class="avatar avatar-small avatar-empty"></span>
{% } %}
<span class="h6 {% if(!data._comments_list.length) { %}text-extra-muted{% } %}">
<span class="list-comment-count small
{% if(!data._comment_count) { %} text-extra-muted {% } else { %} text-muted {% } %}">
<i class="octicon octicon-comment-discussion"></i>
{%= data._comments_list.length || 0 %}
{%= (data._comment_count > 99 ? "99+" : data._comment_count) || 0 %}
</span>
</div>
<div class="pull-right list-row-modified">

View file

@ -2,13 +2,16 @@
<input class="list-delete" type="checkbox"
style="margin: 0 7px 0 0; vertical-align: middle;">
{% } %}
<i class="icon-star
{% if (_starred_by.indexOf(_user)===-1) { %}
text-extra-muted not-starred
{% }%}
icon-fixed-width star-action"
data-name="{{ _name }}" data-doctype="{{ doctype }}">
</i>
<span class="liked-by" data-liked-by=\'{{ JSON.stringify(_liked_by) }}\'>
<i class="octicon octicon-heart
{% if (_liked_by.indexOf(_user)===-1) { %}
text-extra-muted not-liked
{% }%}
icon-fixed-width like-action"
data-name="{{ _name }}" data-doctype="{{ doctype }}">
</i>
<span class="likes-count">{{ (_liked_by.length > 9 ? "9+" : _liked_by.length) || "" }}</span>
</span>
<a class="grey list-id"
style="margin-right: 7px;"
href="#Form/{{ _doctype_encoded }}/{{ _name_encoded }}"

View file

@ -49,7 +49,7 @@ frappe.views.ListView = Class.extend({
}
$.each(['name', 'owner', 'docstatus', '_user_tags', '_comments', 'modified',
'modified_by', '_assign', '_starred_by'],
'modified_by', '_assign', '_liked_by'],
function(i, fieldname) { add_field(fieldname); })
// add title field
@ -284,8 +284,8 @@ frappe.views.ListView = Class.extend({
if(data.modified)
this.prepare_when(data, data.modified);
data._starred_by = data._starred_by ?
JSON.parse(data._starred_by) : [];
data._liked_by = data._liked_by ?
JSON.parse(data._liked_by) : [];
data._checkbox = (frappe.model.can_delete(this.doctype) || this.settings.selectable) && !this.no_delete
@ -311,7 +311,6 @@ frappe.views.ListView = Class.extend({
}
data._user = user;
data._comments_list = data._comments ? JSON.parse(data._comments) : [];
data._tags = $.map((data._user_tags || "").split(","),
function(v) { return v ? v : null; });
data._assign_list = data._assign ? JSON.parse(data._assign) : [];

View file

@ -27,7 +27,7 @@ frappe.get_indicator = function(doc, doctype) {
}
if(is_submittable && doc.docstatus==0) {
return [__("Draft"), "red", "docstatus,=,0"];
return [__("Draft"), "light-blue", "docstatus,=,0"];
}
if(is_submittable && doc.docstatus==2) {

View file

@ -10,7 +10,7 @@ $.extend(frappe.model, {
layout_fields: ['Section Break', 'Column Break', 'Fold'],
std_fields_list: ['name', 'owner', 'creation', 'modified', 'modified_by',
'_user_tags', '_comments', '_assign', '_starred_by', 'docstatus',
'_user_tags', '_comments', '_assign', '_liked_by', 'docstatus',
'parent', 'parenttype', 'parentfield', 'idx'],
std_fields: [
@ -21,7 +21,7 @@ $.extend(frappe.model, {
{fieldname:'modified', fieldtype:'Date', label:__('Last Updated On')},
{fieldname:'modified_by', fieldtype:'Data', label:__('Last Updated By')},
{fieldname:'_user_tags', fieldtype:'Data', label:__('Tags')},
{fieldname:'_starred_by', fieldtype:'Data', label:__('Starred By')},
{fieldname:'_liked_by', fieldtype:'Data', label:__('Liked By')},
{fieldname:'_comments', fieldtype:'Text', label:__('Comments')},
{fieldname:'_assign', fieldtype:'Text', label:__('Assigned To')},
{fieldname:'docstatus', fieldtype:'Int', label:__('Document Status')},

View file

@ -0,0 +1,42 @@
function get_url_arg(name) {
return get_query_params()[name] || "";
}
function get_query_params(query_string) {
var query_params = {};
if (!query_string) {
query_string = location.search.substring(1);
}
var query_list = query_string.split("&");
for (var i=0, l=query_list.length; i < l; i++ ){
var pair = query_list[i].split("=");
var key = pair[0];
if (!key) {
continue;
}
var value = pair[1];
if (typeof value === "string") {
value = decodeURIComponent(value);
}
if (key in query_params) {
if (typeof query_params[key] === undefined) {
query_params[key] = [];
} else if (typeof query_params[key] === "string") {
query_params[key] = [query_params[key]];
}
query_params[key].push(value);
} else {
query_params[key] = value;
}
}
return query_params;
}
function make_query_string(obj) {
var query_params = [];
$.each(obj, function(k, v) { query_params.push(encodeURIComponent(k) + "=" + encodeURIComponent(v)); });
return "?" + query_params.join("&");
}

View file

@ -53,7 +53,15 @@ frappe.route = function() {
frappe.get_route = function(route) {
// for app
return frappe.get_route_str(route).split('/')
var route = frappe.get_route_str(route).split('/')
var parts = route[route.length - 1].split("?");
route[route.length - 1] = parts[0];
if (parts.length > 1) {
var query_params = get_query_params(parts[1]);
frappe.route_options = $.extend(frappe.route_options || {}, query_params);
}
return route;
}
frappe.get_prev_route = function() {

View file

@ -0,0 +1,128 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
frappe.ui.is_liked = function(doc) {
var liked = frappe.ui.get_liked_by(doc);
return liked.indexOf(user)===-1 ? false : true;
}
frappe.ui.get_liked_by = function(doc) {
var liked = doc._liked_by;
if(liked) {
liked = JSON.parse(liked);
}
return liked || [];
}
frappe.ui.toggle_like = function($btn, doctype, name, callback) {
var add = $btn.hasClass("not-liked") ? "Yes" : "No";
// disable click
$btn.css('pointer-events', 'none');
frappe.call({
method: "frappe.desk.like.toggle_like",
quiet: true,
args: {
doctype: doctype,
name: name,
add: add,
},
callback: function(r) {
// renable click
$btn.css('pointer-events', 'auto');
if(!r.exc) {
// update in all local-buttons
var action_buttons = $('.like-action[data-name="'+ name.replace(/"/g, '\"')
+'"][data-doctype="'+ doctype.replace(/"/g, '\"')+'"]');
if(add==="Yes") {
action_buttons.removeClass("not-liked text-extra-muted");
} else {
action_buttons.addClass("not-liked text-extra-muted");
}
// update in locals (form)
var doc = locals[doctype] && locals[doctype][name];
if(doc) {
var liked_by = JSON.parse(doc._liked_by || "[]"),
idx = liked_by.indexOf(user);
if(add==="Yes") {
if(idx===-1)
liked_by.push(user);
} else {
if(idx!==-1) {
liked_by = liked_by.slice(0,idx).concat(liked_by.slice(idx+1))
}
}
doc._liked_by = JSON.stringify(liked_by);
}
if(callback) {
callback();
}
}
}
});
};
frappe.ui.click_toggle_like = function() {
var $btn = $(this);
var $count = $btn.siblings(".likes-count");
var not_liked = $btn.hasClass("not-liked");
var doctype = $btn.attr("data-doctype");
var name = $btn.attr("data-name");
frappe.ui.toggle_like($btn, doctype, name, function() {
if (not_liked) {
$count.text(cint($count.text()) + 1);
} else {
$count.text(cint($count.text()) - 1);
}
});
return false;
}
frappe.ui.setup_like_popover = function($parent, selector) {
if (frappe.dom.is_touchscreen()) {
return;
}
$parent.on("mouseover", selector, function() {
var $wrapper = $(this);
$wrapper.popover({
animation: true,
placement: "right",
content: function() {
var liked_by = JSON.parse($wrapper.attr('data-liked-by') || "[]");
// hack
if ($wrapper.find(".not-liked").length) {
if (liked_by.indexOf(user)!==-1) {
liked_by.splice(liked_by.indexOf(user), 1);
}
} else {
if (liked_by.indexOf(user)===-1) {
liked_by.push(user);
}
}
if (!liked_by.length) {
return "";
}
return frappe.render_template("liked_by", {"liked_by": liked_by});
},
html: true,
container: 'body'
});
$wrapper.popover('show');
});
$parent.on("mouseout", selector, function() {
$(this).popover('destroy');
});
}

View file

@ -0,0 +1,8 @@
<ul class="list-unstyled liked-by-popover">
{% for (var i in liked_by) { var liked_by_user = liked_by[i]; %}
<li>
{%= frappe.avatar(liked_by_user) %}
<span>{%= frappe.user.full_name(liked_by_user) %}</span>
</li>
{% } %}
</ul>

View file

@ -318,7 +318,7 @@ frappe.ui.Listing = Class.extend({
} else {
// no filter for this item,
// setup one
if(['_user_tags', '_comments', '_assign', '_starred_by'].indexOf(fieldname)!==-1) {
if(['_user_tags', '_comments', '_assign', '_liked_by'].indexOf(fieldname)!==-1) {
this.filter_list.add_filter(doctype, fieldname,
'like', '%' + label + '%');
} else {

View file

@ -1,58 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
frappe.ui.is_starred = function(doc) {
var starred = frappe.ui.get_starred_by(doc);
return starred.indexOf(user)===-1 ? false : true;
}
frappe.ui.get_starred_by = function(doc) {
var starred = doc._starred_by;
if(starred) {
starred = JSON.parse(starred);
}
return starred || [];
}
frappe.ui.toggle_star = function($btn, doctype, name) {
var add = $btn.hasClass("not-starred") ? "Yes" : "No";
frappe.call({
method: "frappe.desk.star.toggle_star",
quiet: true,
args: {
doctype: doctype,
name: name,
add: add,
},
callback: function(r) {
if(!r.exc) {
// update in all local-buttons
var action_buttons = $('.star-action[data-name="'+ name.replace(/"/g, '\"')
+'"][data-doctype="'+ doctype.replace(/"/g, '\"')+'"]');
if(add==="Yes") {
action_buttons.removeClass("not-starred").removeClass("text-extra-muted");
} else {
action_buttons.addClass("not-starred").addClass("text-extra-muted");
}
// update in locals (form)
var doc = locals[doctype] && locals[doctype][name];
if(doc) {
var starred_by = JSON.parse(doc._starred_by || "[]"),
idx = starred_by.indexOf(user);
if(add==="Yes") {
if(idx===-1)
starred_by.push(user);
} else {
if(idx!==-1) {
starred_by = starred_by.slice(0,idx).concat(starred_by.slice(idx+1))
}
}
doc._starred_by = JSON.stringify(starred_by);
}
}
}
});
};

View file

@ -4,6 +4,7 @@ frappe.ui.notifications.update_notifications = function() {
frappe.ui.notifications.total = 0;
var doctypes = keys(frappe.boot.notification_info.open_count_doctype).sort();
var modules = keys(frappe.boot.notification_info.open_count_module).sort();
var other = keys(frappe.boot.notification_info.open_count_other).sort();
// clear toolbar / sidebar notifications
frappe.ui.notifications.navbar_notification = $("#navbar-notification").empty();
@ -14,6 +15,12 @@ frappe.ui.notifications.update_notifications = function() {
frappe.ui.notifications.add_notification("ToDo");
frappe.ui.notifications.add_notification("Event");
// add other
$.each(other, function(i, name) {
frappe.ui.notifications.add_notification(name, frappe.boot.notification_info.open_count_other);
});
// add a divider
if(frappe.ui.notifications.total) {
var divider = '<li class="divider"></li>';
@ -34,6 +41,8 @@ frappe.ui.notifications.update_notifications = function() {
var config = frappe.ui.notifications.config[doctype] || {};
if (config.route) {
frappe.set_route(config.route);
} else if (config.click) {
config.click();
} else {
frappe.views.show_open_count_list(this);
}
@ -46,8 +55,12 @@ frappe.ui.notifications.update_notifications = function() {
}
frappe.ui.notifications.add_notification = function(doctype) {
var count = frappe.boot.notification_info.open_count_doctype[doctype];
frappe.ui.notifications.add_notification = function(doctype, notifications_map) {
if(!notifications_map) {
notifications_map = frappe.boot.notification_info.open_count_doctype;
}
var count = notifications_map[doctype];
if(count) {
var config = frappe.ui.notifications.config[doctype] || {};
var label = config.label || doctype;
@ -72,7 +85,21 @@ frappe.ui.notifications.add_notification = function(doctype) {
frappe.ui.notifications.config = {
"ToDo": { label: __("To Do") },
"Comment": { label: __("Messages"), route: "messages"},
"Event": { label: __("Calendar"), route: "Calendar/Event" }
"Event": { label: __("Calendar"), route: "Calendar/Event" },
"Likes": {
label: __("Likes"),
click: function() {
frappe.route_options = {
show_likes: true
};
if (frappe.get_route()[0]=="activity") {
frappe.pages['activity'].on_page_show();
} else {
frappe.set_route("activity");
}
}
}
};
frappe.views.show_open_count_list = function(element) {

View file

@ -40,7 +40,7 @@ frappe.breadcrumbs = {
breadcrumbs.module = frappe.breadcrumbs.preferred[breadcrumbs.doctype];
}
if(breadcrumbs.module && breadcrumbs.module != "Desk") {
if(breadcrumbs.module) {
if(in_list(["Core", "Email", "Custom", "Workflow", "Print"], breadcrumbs.module))
breadcrumbs.module = "Setup";

View file

@ -42,9 +42,7 @@ frappe.views.Calendar = frappe.views.CalendarBase.extend({
var module = locals.DocType[this.doctype].module;
this.page.set_title(__("Calendar") + " - " + __(this.doctype));
if (module !== "Desk") {
frappe.breadcrumbs.add(module, this.doctype)
}
frappe.breadcrumbs.add(module, this.doctype)
this.add_filters();

View file

@ -96,10 +96,10 @@ frappe.views.CommunicationComposer = Class.extend({
var cc = [ [this.frm.doc.owner, 1] ];
var starred_by = frappe.ui.get_starred_by(this.frm.doc);
if (starred_by) {
for ( var i=0, l=starred_by.length; i<l; i++ ) {
cc.push( [starred_by[i], 1] );
var liked_by = frappe.ui.get_liked_by(this.frm.doc);
if (liked_by) {
for ( var i=0, l=liked_by.length; i<l; i++ ) {
cc.push( [liked_by[i], 1] );
}
}

View file

@ -247,7 +247,7 @@ frappe.views.ReportView = frappe.ui.Listing.extend({
"_user_tags": "Tag",
"_comments": "Comment",
"_assign": "Assign",
"_starred_by": "StarredBy",
"_liked_by": "LikedBy",
}[docfield.fieldname] || docfield.fieldtype;
if(docfield.fieldtype==="Link" && docfield.fieldname!=="name") {

View file

@ -472,6 +472,8 @@ _f.Frm.prototype.render_form = function(is_a_different_doc) {
$(cur_frm.wrapper).trigger('render_complete');
this.layout.show_empty_form_message();
this.scroll_to_element();
}
_f.Frm.prototype.refresh_field = function(fname) {
@ -873,3 +875,21 @@ _f.Frm.prototype.get_handlers = function(fieldname, doctype, docname) {
_f.Frm.prototype.has_perm = function(ptype) {
return frappe.perm.has_perm(this.doctype, 0, ptype, this.doc);
}
_f.Frm.prototype.scroll_to_element = function() {
if (frappe.route_options && frappe.route_options.scroll_to) {
var scroll_to = frappe.route_options.scroll_to;
delete frappe.route_options.scroll_to;
var selector = [];
for (var key in scroll_to) {
var value = scroll_to[key];
selector.push(repl('[data-%(key)s="%(value)s"]', {key: key, value: value}));
}
selector = $(selector.join(" "));
if (selector.length) {
frappe.ui.scroll(selector, true, 30);
}
}
}

View file

@ -50,3 +50,13 @@
height: 0;
padding-bottom: 100%;
}
.avatar-frame {
width: 100%;
height: 0;
padding: 50% 0px;
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
border-radius: 4px;
}

View file

@ -408,3 +408,14 @@ ul.linked-with-list li {
border-bottom: 1px solid @border-color;
border-radius: 0px;
}
// like pop-over
.liked-by-popover {
min-width: 200px;
margin-top: -10px;
margin-bottom: -10px;
li {
margin: 15px 0px;
}
}

View file

@ -78,7 +78,7 @@
display: none !important;
}
.shaded-section, .timeline-item:nth-child(even) {
.shaded-section {
background-color: @light-bg;
}
@ -112,47 +112,200 @@
}
.timeline {
border: 1px solid @border-color;
margin: 30px 0px;
.timeline-head {
.comment-input {
height: auto;
}
}
}
.timeline-item {
margin-top: 0px;
padding: 15px 30px 7px;
.icon-fixed-width {
text-align: center;
}
border-bottom: 1px solid @border-color;
blockquote {
font-size: inherit;
}
}
.timeline-item:last-child {
border-bottom: 0px;
.timeline-items {
position: relative;
}
.timeline-item .reply {
margin-top: 5px;
// padding-left: 24px;
// border-left: 8px solid #d8dfe6;
.timeline {
position: relative;
}
.timeline::before {
content: " ";
border-left: 1px solid @border-color;
position: absolute;
top: 0px;
bottom: -124px;
left: 43px;
z-index: -1;
}
@media(max-width: @screen-sm) {
.timeline::before {
bottom: -64px;
}
}
.timeline-item.user-content {
margin: 30px 0px 30px 30px;
.media-body {
border: 1px solid @border-color;
border-radius: 2px;
margin-left: -7px;
position: relative;
// to display the triangle beside the box
overflow: visible;
}
.comment-header {
background-color: @light-bg;
padding: 7px 15px;
margin: 0px;
color: @text-muted;
border-bottom: 1px solid @light-border-color;
.octicon-heart {
color: @heart-color;
cursor: pointer;
}
}
.reply {
padding: 10px 15px;
& > div > p:first-child {
margin-top: 0px;
}
& > div > p:last-child {
margin-bottom: 0px;
}
}
.close-btn-container {
padding: 2px 15px;
}
.comment-likes {
margin-left: 5px;
}
.media-body:after, .media-body:before {
right: 100%;
top: 15px;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
.media-body:after {
border-color: rgba(136, 183, 213, 0);
border-right-color: #fafbfc;
border-width: 6px;
margin-top: -6px;
}
.media-body:before {
border-color: rgba(194, 225, 245, 0);
border-right-color: @border-color;
border-width: 7px;
margin-top: -7px;
}
}
.timeline-item.notification-content {
padding-left: 30px;
margin: 30px 0px;
position: relative;
color: @text-muted;
* {
color: @text-muted;
}
.icon-fixed-width {
margin-left: 36px;
}
.octicon-heart {
color: @heart-color !important;
}
}
.timeline-indicator() {
content: " ";
width: 7px;
height: 7px;
background-color: @border-color;
// background-color: white;
// border: 1px solid @border-color;
position: absolute;
left: 40px;
border-radius: 50%;
top: 5px;
}
.timeline-item.notification-content::before {
.timeline-indicator();
}
.timeline-item .reply-link {
padding: 0px 7px;
}
.timeline-item h6, .timeline-head h6 {
margin-top: 6px;
padding-left: 7px;
}
.timeline-head {
background-color: @light-bg;
padding: 15px 30px;
border-bottom: 1px solid @border-color;
background-color: white;
// padding: 15px 30px;
border: 1px solid @border-color;
border-radius: 2px;
.comment-input-header {
background-color: @light-bg;
padding: 7px 15px;
border-bottom: 1px solid @light-border-color;
}
.comment-input-container {
padding: 15px;
}
.comment-input {
border-color: @light-border-color;
max-width: 100%;
&:focus {
box-shadow: none;
}
}
}
@media(max-width: @screen-xs) {
.timeline-head {
border-left: none;
border-right: none;
border-radius: 0px;
}
}
.timeline-new-email {
margin: 30px 0px;
padding: 0px 65px;
position: relative;
}
.timeline-new-email::before {
.timeline-indicator();
}
.form-footer h5 {

View file

@ -54,3 +54,13 @@
.indicator-right.darkgrey::after {
background: @indicator-darkgrey;
}
.indicator.yellow::before,
.indicator-right.yellow::after {
background: @indicator-yellow;
}
.indicator.light-blue::before,
.indicator-right.light-blue::after {
background: @indicator-light-blue;
}

View file

@ -124,6 +124,14 @@
.doclist-row {
font-size: 12px;
.likes-count {
display: inline-block;
width: 15px;
margin-left: -5px;
color: @text-muted;
font-size: 12px;
}
}
.doclist-row .docstatus .octicon {
@ -172,16 +180,17 @@
padding: 5px 15px;
}
.listview-main-section .icon-star {
.listview-main-section .octicon-heart {
cursor: pointer;
}
.list-row-head .icon-star {
vertical-align: middle;
.list-row-head .octicon-heart {
margin-right: 13px;
}
.star-action.icon-star {
color: #ffdb4c;
.like-action.octicon-heart {
// color: #ffdb4c;
color: @heart-color;
}
.list-id {
@ -209,3 +218,9 @@
display: none;
}
}
.list-comment-count {
display: inline-block;
width: 37px;
text-align: left;
}

View file

@ -261,26 +261,6 @@
border-right-color: transparent !important;
}
// timeline
.timeline {
border-left: none !important;
border-right: none !important;
.timeline-head {
padding: 7px 15px;
}
.timeline-item {
padding: 15px;
border-bottom: 1px dashed @border-color;
}
.timeline-item:last-child {
border-bottom: none;
}
}
// listviews
.list-row {
padding: 13px 15px !important;

View file

@ -146,6 +146,11 @@ body[data-route^="Module"] .main-menu {
}
}
}
.liked-by .octicon-heart {
font-size: 16px;
cursor: pointer;
}
}
.form-sidebar .form-shared .share-doc-btn& {
@ -164,6 +169,7 @@ body[data-route^="Module"] .main-menu {
.form-tags,
.assignment-row,
.form-shared,
.liked-by,
.modified-by,
.created-by,
.tags-label,

View file

@ -24,6 +24,10 @@
@indicator-orange: #ffa00a;
@indicator-purple: #743ee2;
@indicator-darkgrey: #b8c2cc;
@indicator-yellow: #FEEF72;
@indicator-light-blue:#7CD6FD;
@heart-color: @indicator-red;
@navbar-default-color: #6C7680;
@navbar-inverse-color: #9D9D9D;

View file

@ -10,6 +10,9 @@ import re, urllib, datetime, math
import babel.dates
from dateutil import parser
from num2words import num2words
import HTMLParser
from html2text import html2text
DATE_FORMAT = "%Y-%m-%d"
TIME_FORMAT = "%H:%M:%S.%f"
@ -620,3 +623,12 @@ def unique(seq):
def strip(val, chars=None):
# \ufeff is no-width-break, \u200b is no-width-space
return (val or "").replace("\ufeff", "").replace("\u200b", "").strip(chars)
def to_markdown(html):
text = None
try:
text = html2text(html)
except HTMLParser.HTMLParseError:
pass
return text

View file

@ -506,46 +506,6 @@ function valid_email(id) {
var validate_email = valid_email;
function get_url_arg(name) {
return get_query_params()[name] || "";
}
function get_query_params() {
var query_params = {};
var query_string = location.search.substring(1);
var query_list = query_string.split("&");
for (var i=0, l=query_list.length; i < l; i++ ){
var pair = query_list[i].split("=");
var key = pair[0];
if (!key) {
continue;
}
var value = pair[1];
if (typeof value === "string") {
value = decodeURIComponent(value);
}
if (key in query_params) {
if (typeof query_params[key] === undefined) {
query_params[key] = [];
} else if (typeof query_params[key] === "string") {
query_params[key] = [query_params[key]];
}
query_params[key].push(value);
} else {
query_params[key] = value;
}
}
return query_params;
}
function make_query_string(obj) {
var query_params = [];
$.each(obj, function(k, v) { query_params.push(encodeURIComponent(k) + "=" + encodeURIComponent(v)); });
return "?" + query_params.join("&");
}
function repl(s, dict) {
if(s==null)return '';
for(key in dict) {