Merged from develop

This commit is contained in:
scmmishra 2019-03-14 11:36:36 +05:30
commit c03e6a08e9
42 changed files with 1018 additions and 677 deletions

View file

@ -56,6 +56,7 @@
"root": true,
"globals": {
"frappe": true,
"Vue": true,
"__": true,
"_p": true,
"_f": true,
@ -112,11 +113,9 @@
"getCookie": true,
"getCookies": true,
"get_url_arg": true,
"md5": true,
"$": true,
"jQuery": true,
"Vue": true,
"moment": true,
"hljs": true,
"Awesomplete": true,

View file

@ -53,6 +53,7 @@ def handle():
if call=="method":
frappe.local.form_dict.cmd = doctype
frappe.local.form_dict.pop("data", None)
return frappe.handler.handle()
elif call=="resource":

View file

@ -98,6 +98,7 @@ def load_conf_settings(bootinfo):
def load_desktop_icons(bootinfo):
from frappe.config import get_modules_from_all_apps_for_user
bootinfo.allowed_modules = get_modules_from_all_apps_for_user()
bootinfo.home_settings = frappe.db.get_value("User", frappe.session.user, 'home_settings','')
def get_allowed_pages():
return get_user_pages_or_reports('Page')

View file

@ -7,6 +7,7 @@ from frappe import _
import frappe.model
import frappe.utils
import json, os
from frappe.utils import get_safe_filters
from six import iteritems, string_types, integer_types
@ -370,17 +371,4 @@ def check_parent_permission(parent, child_doctype):
if frappe.permissions.has_permission(parent):
return
# Either parent not passed or the user doesn't have permission on parent doctype of child table!
raise frappe.PermissionError
def get_safe_filters(filters):
try:
filters = json.loads(filters)
if isinstance(filters, (integer_types, float)):
filters = frappe.as_unicode(filters)
except (TypeError, ValueError):
# filters are not passesd, not json
pass
return filters
raise frappe.PermissionError

View file

@ -11,28 +11,22 @@ def get_modules_from_all_apps_for_user(user=None):
all_modules = get_modules_from_all_apps()
user_blocked_modules = frappe.get_doc('User', user).get_blocked_modules()
allowed_modules_list = [m for m in all_modules if m.get("module_name") not in user_blocked_modules]
empty_tables_by_module = get_all_empty_tables_by_module()
home_settings = frappe.db.get_value("User", frappe.session.user, 'home_settings')
if home_settings:
home_settings = json.loads(home_settings)
for module in allowed_modules_list:
module_name = module["module_name"]
module_name = module.get("module_name")
# Apply onboarding status
if module_name in empty_tables_by_module:
module["onboard_present"] = 1
if home_settings:
category_settings = home_settings.get(module.get("category"), {}) if module.get("category") else {}
if module_name not in category_settings:
module["hidden"] = 1
else:
links = category_settings[module_name]["links"]
if links:
module["links"] = get_module_link_items_from_list(module["app"], module_name, links.split(","))
# Set defaults links
module["links"] = get_onboard_items(module["app"], frappe.scrub(module_name))[:5]
else:
module["links"] = get_onboard_items(module["app"], frappe.scrub(module_name))[:6]

View file

@ -20,6 +20,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "viewed_by",
"fieldtype": "Data",
"hidden": 0,
@ -40,7 +41,7 @@
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"search_index": 1,
"set_only_once": 1,
"translatable": 0,
"unique": 0
@ -52,6 +53,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "reference_doctype",
"fieldtype": "Link",
"hidden": 0,
@ -73,7 +75,7 @@
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"search_index": 1,
"set_only_once": 1,
"translatable": 0,
"unique": 0
@ -85,6 +87,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"hidden": 0,
@ -106,7 +109,7 @@
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"search_index": 1,
"set_only_once": 1,
"translatable": 0,
"unique": 0
@ -122,7 +125,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-01-03 13:04:31.389182",
"modified": "2019-03-11 18:28:33.277683",
"modified_by": "Administrator",
"module": "Core",
"name": "View Log",

View file

@ -17,6 +17,9 @@ def get_notification_config():
"for_other": {
"Likes": "frappe.core.notifications.get_unseen_likes",
"Email": "frappe.core.notifications.get_unread_emails",
},
"for_module": {
"Social": "frappe.social.doctype.post.post.get_unseen_post_count"
}
}

View file

@ -10,21 +10,14 @@ frappe.listview_settings['ToDo'] = {
},
hide_name_column: true,
refresh: function(me) {
// override assigned to me by owner
if (me.todo_sidebar_setup) return;
me.page.sidebar.find(".assigned-to-me a").off("click").on("click", function() {
me.filter_area.remove("assigned_by");
me.filter_area.add([[me.doctype, "owner", '=', frappe.session.user]]);
});
// add assigned by me
me.page.add_sidebar_item(__("Assigned By Me"), function() {
me.filter_area.remove("owner");
me.filter_area.add([[me.doctype, "assigned_by", '=', frappe.session.user]]);
}, ".assigned-to-me");
}, ('.list-link[data-view="Kanban"]'));
me.todo_sidebar_setup = true;
},
add_fields: ["reference_type", "reference_name"],
}
}

View file

@ -24,3 +24,10 @@ def set_list_settings(doctype, values):
frappe.clear_messages()
doc.update(json.loads(values))
doc.save()
@frappe.whitelist()
def get_user_assignments_and_count():
user_list = frappe.get_list("User", filters={"user_type": "System User"})
assignment_data = sorted([{"count":frappe.db.count('ToDo', filters = {'reference_type': 'Issue', 'owner': user['name'], 'status': 'Open'}),
"name": user['name']} for user in user_list], key=lambda k: k['count'], reverse = True)
return assignment_data

View file

@ -300,18 +300,32 @@ def get_links(app, module):
for section in sections:
for item in section["items"]:
link_names.append(item.get("label"))
print(link_names)
return link_names
@frappe.whitelist()
def get_module_link_items_from_dict(module_link_list_map):
module_link_list_map = json.loads(module_link_list_map)
module_links = {}
for module, data in module_link_list_map.items():
print(data)
module_links[module] = get_module_link_items_from_list(data["app"], module, data["links"])
return module_links
def update_desk_section_settings(desk_section, new_settings):
home_settings = frappe.db.get_value("User", frappe.session.user, 'home_settings')
if home_settings:
home_settings = json.loads(home_settings)
else:
return {}
new_settings = json.loads(new_settings)
for module, data in new_settings.items():
if data.get("links"):
data["links"] = get_module_link_items_from_list(data["app"], module, data.get("links"))
data.pop("app", None)
home_settings[desk_section] = new_settings
settings_json_str = json.dumps(home_settings)
# # This didn't work
# frappe.db.set_value("User", frappe.session.user, 'home_settings', json.dumps(home_settings))
frappe.db.sql("""update tabUser set home_settings = %s""", (settings_json_str), debug=True)
frappe.db.commit()
return new_settings
def get_module_link_items_from_list(app, module, list_of_link_names):

View file

@ -1154,7 +1154,7 @@ class Document(BaseDocument):
frappe.local.flags.commit = True
def add_viewed(self, user=None):
'''add log to communication when a user viewes a document'''
'''add log to communication when a user views a document'''
if not user:
user = frappe.session.user

View file

@ -14,6 +14,7 @@ execute:frappe.reload_doc('core', 'doctype', 'comment')
frappe.patches.v8_0.drop_is_custom_from_docperm
execute:frappe.reload_doc('core', 'doctype', 'module_def') #2017-09-22
execute:frappe.reload_doc('core', 'doctype', 'version') #2017-04-01
execute:frappe.reload_doc('email', 'doctype', 'document_follow')
frappe.patches.v11_0.replicate_old_user_permissions
frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03
frappe.patches.v7_1.rename_scheduler_log_to_error_log
@ -200,7 +201,6 @@ frappe.patches.v9_1.move_feed_to_activity_log
execute:frappe.delete_doc('Page', 'data-import-tool', ignore_missing=True)
frappe.patches.v10_0.reload_countries_and_currencies
frappe.patches.v10_0.refactor_social_login_keys
execute:frappe.reload_doc('email', 'doctype', 'document_follow')
frappe.patches.v10_0.enable_chat_by_default_within_system_settings
frappe.patches.v10_0.remove_custom_field_for_disabled_domain
execute:frappe.delete_doc("Page", "chat")
@ -232,8 +232,7 @@ frappe.patches.v11_0.set_missing_creation_and_modified_value_for_user_permission
frappe.patches.v11_0.remove_doctype_user_permissions_for_page_and_report
frappe.patches.v11_0.set_default_letter_head_source
frappe.patches.v12_0.set_primary_key_in_series
execute:frappe.reload_doc('email', 'doctype', 'document_follow')
execute:frappe.delete_doc("Page", "modules", ignore_missing=True)
frappe.patches.v11_0.set_default_letter_head_source
frappe.patches.v12_0.setup_comments_from_communications
frappe.patches.v12_0.init_desk_settings
frappe.patches.v12_0.init_desk_settings #11-03-2019

View file

@ -6,29 +6,6 @@ from frappe.config import get_modules_from_all_apps_for_user
from frappe.desk.moduleview import get_onboard_items
def execute():
"""Set the initial customizations for desk, with modules, indices and links."""
"""Reset the initial customizations for desk, with modules, indices and links."""
frappe.reload_doc("core", "doctype", "user")
all_modules = get_modules_from_all_apps_for_user()
settings = {}
for module in all_modules:
if not module.get("app"): continue
links = get_onboard_items(module["app"], frappe.scrub(module["module_name"]))[:5]
module_settings = {
"links": ",".join([d["label"] for d in links])
}
category_dict = settings.get(module.get("category", ""), None)
if category_dict:
module_settings["index"] = len(category_dict)
category_dict[module.get("module_name")] = module_settings
else:
module_settings["index"] = 0
settings[module.get("category", "")] = {
module.get("module_name"): module_settings
}
settings_json_str = json.dumps(settings)
frappe.db.sql("""update tabUser set home_settings = %s""", (settings_json_str), debug=True)
frappe.db.sql("""update tabUser set home_settings = %s""", (''), debug=True)

View file

@ -22,9 +22,6 @@
"js/chat.js": [
"public/js/frappe/chat.js"
],
"js/frappe-vue.min.js": [
"public/js/frappe_vue.js"
],
"js/frappe-recorder.min.js": [
"public/js/frappe/recorder/recorder.js"
],

View file

@ -39,6 +39,7 @@ frappe.Application = Class.extend({
throw 'boot failed';
}
this.setup_frappe_vue();
this.load_bootinfo();
this.load_user_permissions();
this.make_nav_bar();
@ -106,6 +107,8 @@ frappe.Application = Class.extend({
dialog.get_close_btn().toggle(false);
});
this.setup_social_listeners();
// listen to build errors
this.setup_build_error_listener();
@ -119,6 +122,12 @@ frappe.Application = Class.extend({
}
},
setup_frappe_vue() {
Vue.prototype.__ = window.__;
Vue.prototype.frappe = window.frappe;
},
set_password: function(user) {
var me=this;
frappe.call({
@ -530,6 +539,14 @@ frappe.Application = Class.extend({
console.log(data);
});
}
},
setup_social_listeners() {
frappe.realtime.on('mention', (message) => {
if (frappe.get_route()[0] !== 'social') {
frappe.show_alert(message);
}
});
}
});

View file

@ -68,7 +68,7 @@ frappe.dom = {
return txt;
}
},
is_element_in_viewport: function (el) {
is_element_in_viewport: function (el, tolerance=0) {
//special bonus for those using jQuery
if (typeof jQuery === "function" && el instanceof jQuery) {
@ -78,10 +78,10 @@ frappe.dom = {
var rect = el.getBoundingClientRect();
return (
rect.top >= 0
&& rect.left >= 0
// && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
// && rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
rect.top + tolerance >= 0
&& rect.left + tolerance >= 0
&& rect.bottom - tolerance <= $(window).height()
&& rect.right - tolerance <= $(window).width()
);
},

View file

@ -368,7 +368,6 @@ frappe.ui.form.Timeline = class Timeline {
} else {
c.content_html = c.content;
c.content_html = frappe.utils.strip_whitespace(c.content_html);
c.content_html = c.content_html.replace(/&lt;/g,"<").replace(/&gt;/g,">");
}
// bold @mentions

View file

@ -45,8 +45,12 @@
</ul>
</div>
</li>
<li class="assigned-to-me">
<a>{%= __("Assigned To Me") %}</a>
<li class="assigned-to">
<a class = "dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{%= __("Assigned To") %} <span class="caret"></span>
</a>
<ul class="dropdown-menu assigned-dropdown" style="max-height: 300px; overflow-y: auto;">
</ul>
</li>
{% if(frappe.help.has_help(doctype)) { %}
<li><a class="help-link" data-doctype="{{ doctype }}">{{ __("Help") }}</a></li>

View file

@ -26,7 +26,7 @@ frappe.views.ListSidebar = class ListSidebar {
this.setup_reports();
this.setup_list_filter();
this.setup_assigned_to_me();
this.setup_assigned_to();
this.setup_views();
this.setup_kanban_boards();
this.setup_calendar_view();
@ -216,12 +216,35 @@ frappe.views.ListSidebar = class ListSidebar {
});
}
setup_assigned_to_me() {
this.page.sidebar.find(".assigned-to-me a").on("click", () => {
this.list_view.filter_area.add(this.list_view.doctype, "_assign", "like", `%${frappe.session.user}%`);
setup_assigned_to() {
let dropdown = this.page.sidebar.find('.assigned-dropdown');
if(this.doctype === 'ToDo') {
$('.assigned-to').remove();
}
frappe.call('frappe.desk.listview.get_user_assignments_and_count').then((data) => {
let current_user_count = data.message.find(user => user.name === frappe.session.user).count;
this.get_html_for_assigned(frappe.session.user, current_user_count).appendTo(dropdown);
let user_list = data.message.filter(user => !['Guest', frappe.session.user, 'Administrator'].includes(user.name));
user_list.forEach((user) => {
this.get_html_for_assigned(user.name, user.count).appendTo(dropdown);
});
$(".assigned-dropdown li a").on("click", (e) => {
let assigned_user = $(e.currentTarget).find($('.assigned-user')).text();
if(assigned_user === 'Me') assigned_user = frappe.session.user;
this.list_view.filter_area.remove('_assign');
this.list_view.filter_area.add(this.list_view.doctype, "_assign", "like", `%${assigned_user}%`);
});
});
}
get_html_for_assigned(name, count) {
if (name === frappe.session.user) name='Me';
if (count > 99) count='99+';
let html = $('<li class="assigned"><a class="badge-hover" role="assigned-item"><span class="assigned-user">'
+ name + '</span><span class="badge pull-right" style="position:relative">' + count + '</span></a></li>');
return html;
}
setup_upgrade_box() {
let upgrade_list = $(`<ul class="list-unstyled sidebar-menu"></ul>`).appendTo(this.sidebar);

View file

@ -11,6 +11,7 @@
import Wall from './pages/Wall.vue';
import Profile from './pages/Profile.vue';
import UserList from './pages/UserList.vue';
import NotFound from './components/NotFound.vue';
import ImageViewer from './components/ImageViewer.vue';
@ -27,6 +28,10 @@ function get_route_map() {
'key': frappe.get_route()[2]
}
},
'social/users': {
'component': UserList,
'props': {}
},
'not_found': {
'component': NotFound,
}
@ -58,11 +63,14 @@ export default {
frappe.app_updates.on('user_image_updated', () => {
this.$root.$emit('user_image_updated')
})
this.update_primary_action(frappe.get_route()[1])
},
mounted() {
frappe.route.on('change', () => {
if (frappe.get_route()[0] === 'social') {
this.set_current_page();
this.update_primary_action(frappe.get_route()[1])
frappe.utils.scroll_to(0);
$("body").attr("data-route", frappe.get_route_str());
}
@ -73,6 +81,15 @@ export default {
set_current_page() {
this.current_page = this.get_current_page();
},
update_primary_action(current_route) {
if (current_route === 'home') {
this.$root.page.set_primary_action(__('Post'), () => {
frappe.social.post_dialog.show();
});
} else {
this.$root.page.clear_primary_action()
}
},
get_current_page() {
const route_map = get_route_map();
const route = frappe.get_route_str();

View file

@ -12,6 +12,12 @@
v-if="options.length"
:options="options"
/>
<transition name="fade">
<span
class="indicator blue"
v-if="!this.post.seen">
</span>
</transition>
</div>
<div class="user-avatar" v-html="user_avatar" @click="goto_profile(post.owner)"></div>
<a class="user-name" @click="goto_profile(post.owner)">{{ user_name }}</a>
@ -170,6 +176,11 @@ export default {
}).then(frappe.dom.unfreeze)
})
},
update_seen() {
frappe.xcall('frappe.social.doctype.post.post.set_seen', {
post_name: this.post.name
}).then(() => this.post.seen = true)
},
generate_preview(link_element) {
// TODO: move the code to separate component
frappe.xcall('frappe.social.doctype.post.post.get_link_info', {
@ -203,5 +214,14 @@ export default {
padding-top: 0px;
background: #F6F6F6;
}
.indicator {
margin-left: 15px;
}
.fade-enter-active, .fade-leave-active {
transition: opacity .8s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>

View file

@ -44,7 +44,7 @@ export default {
cursor: pointer;
color: #8d99a6;
span {
padding-left: 5px;
padding: 5px;
}
&:hover {
color: darken(#8d99a6, 10%);

View file

@ -2,26 +2,30 @@
<div>
<div class="comment-box flex-column">
<div class="text-muted comment-label">{{ __('Add a comment') }}</div>
<textarea v-model="comment_content"></textarea>
<button
:disabled="comment_content === ''"
class="btn btn-primary btn-sm"
@click="create_comment">
{{ __('Comment') }}
</button>
<div ref="comment-section"></div>
<div class="flex justify-between">
<div class="text-muted small">
{{ __("Ctrl+Enter to add comment") }}
</div>
<button
class="btn btn-primary btn-sm"
@click="create_comment">
{{ __('Comment') }}
</button>
</div>
</div>
<div v-if="comments.length" class="comment-list">
<div ref="comments" v-if="comments.length" class="comment-list">
<div class="comment" v-for="comment in comments" :key="comment.name">
<span
class="pull-right text-muted"
v-html="get_time(comment.creation)">
</span>
<span
class="cursor-pointer"
@click="go_to_profile_page(comment.owner)"
v-html="get_avatar(comment.owner)">
</span>
<span>{{ comment.content }}</span>
<span class="content" v-html="comment.content"/>
<span
class="text-muted"
v-html="get_time(comment.creation)">
</span>
</div>
</div>
</div>
@ -29,10 +33,9 @@
<script>
export default {
props: ['comments'],
data() {
return {
comment_content: ''
}
mounted() {
this.make_comment_section();
this.make_mentions_clickable(this.$refs['comments']);
},
methods: {
get_avatar(user) {
@ -41,12 +44,45 @@ export default {
get_time(timestamp) {
return comment_when(timestamp, true)
},
create_comment() {
this.$emit('create_comment', this.comment_content);
this.comment_content = '';
},
go_to_profile_page(user) {
frappe.set_route('social', 'profile', user)
},
make_comment_section() {
this.comment_section = frappe.ui.form.make_control({
parent: this.$refs['comment-section'],
only_input: true,
render_input: true,
no_wrapper: true,
mentions: this.get_names_for_mentions(),
df: {
fieldtype: 'Comment',
fieldname: 'comment'
},
on_submit: this.create_comment.bind(this)
});
},
create_comment() {
const message = this.comment_section.get_value().replace('<div><br></div>', '');
if (!strip_html(message)) return
frappe.utils.play_sound("click");
this.$emit('create_comment', message);
this.comment_section.clear();
},
get_names_for_mentions() {
var valid_users = Object.keys(frappe.boot.user_info)
.filter(user => !["Administrator", "Guest"].includes(user));
valid_users = valid_users
.filter(user => frappe.boot.user_info[user].allowed_in_mentions==1);
return valid_users.map(user => frappe.boot.user_info[user].name);
},
make_mentions_clickable(parent_element) {
Array.from(parent_element.getElementsByClassName('mention'))
.forEach((mention) => {
mention.classList.add('cursor-pointer');
mention.addEventListener('click', () => {
this.go_to_profile_page(mention.dataset.value)
})
});
}
}
}
@ -56,16 +92,11 @@ export default {
.comment-label {
margin-bottom: 5px;
}
textarea {
width: 100%;
::v-deep .ql-editor {
background: white;
border-radius: 4px;
outline: none;
border: none;
margin-bottom: 15px;
height: 60px;
padding: 5px;
min-height: 60px !important;
border: 1px solid #d1d8dd;
resize: none;
}
button {
padding: 2px 5px;
@ -76,7 +107,17 @@ export default {
.comment-list {
margin-top: 10px;
.comment {
.comment-input-wrapper {
margin-top: -6px;
font-size: 11px;
}
display: flex;
padding: 5px 0;
.content {
align-self: center;
font-size: 12px;
flex: 1
}
}
}
</style>

View file

@ -1,8 +1,10 @@
<template>
<div>
<post-skeleton v-for="index in 5" :key="index" v-if="loading_posts && !posts.length"/>
<div v-if="loading_posts && !posts.length">
<post-skeleton v-for="index in 5" :key="index"/>
</div>
<transition-group name="flip-list">
<post
<post ref="posts"
:post="post"
v-for="(post, index) in posts"
:key="post.name"
@ -61,11 +63,9 @@ export default {
},
methods: {
get_posts(filters, load_old) {
return frappe.db.get_list('Post', {
fields: ['name', 'content', 'owner', 'creation', 'liked_by', 'is_pinned', 'is_globally_pinned'],
filters: filters,
limit_start: load_old ? this.posts.length : 0,
order_by: 'is_globally_pinned desc, creation desc',
return frappe.xcall('frappe.social.doctype.post.post.get_posts', {
filters,
limit_start: load_old ? this.posts.length : 0
})
},
update_posts(load_old = false) {
@ -85,9 +85,11 @@ export default {
}
}).finally(() => {
this.loading_posts = false;
this.track_seen()
});
},
handle_scroll: frappe.utils.debounce(function() {
this.track_seen()
const screen_bottom = document.documentElement.scrollTop + window.innerHeight === document.documentElement.offsetHeight;
if (screen_bottom && this.more_posts_available) {
if (!this.loading_posts) {
@ -95,6 +97,15 @@ export default {
}
}
}, 500),
track_seen() {
const posts = this.$refs.posts || []
posts.forEach((post_component) => {
if(!post_component.post.seen
&& frappe.dom.is_element_in_viewport(post_component.$el, 50)) {
post_component.update_seen()
}
})
},
delete_post(index) {
this.posts.splice(index, 1);
},

View file

@ -1,9 +1,9 @@
<template>
<div class="flex flex-column">
<div class="user-details">
<div class="user-avatar" v-html="user_avatar"></div>
<a class="user_name" @click="go_to_profile_page()">{{ user.fullname }}</a>
</div>
<a class="route-link"
@click.prevent="go_to_user_list()">
{{ __('All Users') }}
</a>
<div class="links" v-if="frequently_visited_list.length">
<div class="muted-title">
{{ __('Frequently Visited Links') }}
@ -24,8 +24,6 @@ export default {
data() {
return {
frequently_visited_list: [],
user: frappe.user_info(frappe.session.user),
user_avatar: frappe.avatar(this.user_id, 'avatar-xl')
}
},
created() {
@ -45,7 +43,10 @@ export default {
return frappe.utils.get_route_label(route);
},
go_to_profile_page() {
frappe.set_route('social', 'profile', this.user.name)
frappe.set_route('social', 'profile', frappe.session.user)
},
go_to_user_list() {
frappe.set_route('social', 'users')
}
}
}
@ -58,9 +59,6 @@ export default {
.stats {
min-height: 150px
}
.links {
margin-top: 20px;
}
.user-details {
.user-avatar {
/deep/.avatar-xl {

View file

@ -0,0 +1,95 @@
<template>
<div class="flex justify-center">
<div class="col-md-6">
<div class="flex justify-between padding search-bar">
<div class="flex col-md-6">
<button class="btn" @click="frappe.set_route('social', 'home')"> {{ __('Back') }}</button>
<input type="text" class="form-control" :placeholder="__('Search for a user...')" v-model="search_text">
</div>
</div>
<ul class="list-unstyled user-list">
<li
class="padding cursor-pointer flex user-card"
v-for="user in filtered_users" :key="user.name"
@click="go_to_profile_page(user.name)">
<span v-html="get_avatar(user.name)"></span>
<div class="user-details">
{{ user.fullname }}
<div class="text-muted text-medium" :class="{'italic': !user.bio}">{{ frappe.ellipsis(user.bio, 100) || 'No Bio'}}</div>
</div>
</li>
<li class="text-muted" v-if="!filtered_users.length">{{__('No user found')}}</li>
</ul>
</div>
</div>
</template>
<script>
export default {
data() {
return {
users: [],
search_text: ''
}
},
computed: {
filtered_users() {
let filtered = this.users;
if (this.search_text !== '') {
filtered = filtered.filter(user => {
if (user.fullname.toLowerCase().includes(this.search_text.toLowerCase())) {
return true;
}
})
}
return filtered;
}
},
created() {
const standard_users = ['Administrator', 'Guest', 'guest@example.com'];
this.users = frappe.boot.user_info;
// delete standard users from the list
standard_users.forEach(user => delete this.users[user]);
this.users = Object.values(this.users);
},
methods: {
get_avatar(user) {
return frappe.avatar(user, 'avatar-medium')
},
go_to_profile_page(user) {
frappe.set_route('social', 'profile', user)
}
}
}
</script>
<style lang="less" scoped>
.user-list {
// similar to search bar height
margin-top: 75px;
.user-card {
&:hover {
border: 1px solid #d1d8dd;
}
border-radius: 5px;
.user-details {
margin-left: 10px;
.italic {
font-style: italic;
}
}
}
}
.search-bar {
position: fixed;
background: white;
height: 75px;
text-align: center;
div {
margin: auto;
}
width: 100%;
left: 0;
}
</style>

View file

@ -8,25 +8,20 @@ frappe.social.Home = class SocialHome {
this.page = parent.page;
this.setup_header();
this.make_body();
this.set_primary_action();
}
make_body() {
this.$social_container = this.$parent.find('.layout-main');
frappe.require('/assets/js/frappe-vue.min.js', () => {
new Vue({
el: this.$social_container[0],
render: h => h(Home)
});
new Vue({
el: this.$social_container[0],
render: h => h(Home),
data: {
'page': this.page
}
});
}
setup_header() {
this.page.set_title(__('Social'));
}
set_primary_action() {
this.page.set_primary_action(__('Post'), () => {
frappe.social.post_dialog.show();
});
}
};
frappe.social.post_dialog = new frappe.ui.Dialog({

View file

@ -85,26 +85,30 @@ frappe.ui.click_toggle_like = function() {
return false;
}
frappe.ui.setup_like_popover = function($parent, selector, check_not_liked=true) {
frappe.ui.setup_like_popover = ($parent, selector, check_not_liked=true) => {
if (frappe.dom.is_touchscreen()) {
return;
}
$parent.on("mouseover", selector, function() {
var $wrapper = $(this);
$wrapper.popover({
$parent.on('mouseover', selector, function() {
const target_element = $(this);
target_element.popover({
animation: true,
placement: "right",
content: function() {
var liked_by = $wrapper.attr('data-liked-by');
placement: 'right',
trigger: 'manual',
template:`<div class="liked-by-popover popover">
<div class="arrow"></div>
<div class="popover-content"></div>
</div>`,
content: () => {
let liked_by = target_element.attr('data-liked-by');
liked_by = liked_by ? decodeURI(liked_by) : '[]';
liked_by = JSON.parse(liked_by);
var user = frappe.session.user;
const user = frappe.session.user;
// hack
if (check_not_liked) {
if ($wrapper.find(".not-liked").length) {
if (target_element.find(".not-liked").length) {
if (liked_by.indexOf(user)!==-1) {
liked_by.splice(liked_by.indexOf(user), 1);
}
@ -118,16 +122,46 @@ frappe.ui.setup_like_popover = function($parent, selector, check_not_liked=true)
if (!liked_by.length) {
return "";
}
return frappe.render_template("liked_by", {"liked_by": liked_by});
let liked_by_list = $(`<ul class="list-unstyled"></ul>`);
// to show social profile of the user
let link_base = '#social/profile/';
liked_by.forEach(user => {
// append user list item
liked_by_list.append(`
<li data-user=${user}>${frappe.avatar(user)}
<span>${frappe.user.full_name(user)}</span>
</li>
`);
});
liked_by_list.children('li').click(ev => {
let user = ev.currentTarget.dataset.user;
target_element.popover('hide');
frappe.set_route(link_base + user);
});
return liked_by_list;
},
html: true,
container: 'body'
});
$wrapper.popover('show');
target_element.popover('show');
$(".popover").on("mouseleave", () => {
target_element.popover('hide');
});
target_element.on('mouseout', () => {
setTimeout(() => {
if (!$('.popover:hover').length) {
target_element.popover('hide');
}
}, 100);
});
});
$parent.on("mouseout", selector, function() {
$(this).popover('destroy');
});
}
};

View file

@ -1,8 +0,0 @@
<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

@ -28,192 +28,216 @@ import DeskModuleBox from "./DeskModuleBox.vue";
import { generate_route } from "./utils.js";
export default {
props: ["category", "all_modules"],
components: {
DeskModuleBox
},
data() {
let template_modules = this.all_modules;
template_modules.forEach(module => {
if (module.links) {
module.links.forEach(link => {
link.route = generate_route(link);
});
}
});
props: ['category', 'all_modules', 'customization_settings'],
components: {
DeskModuleBox
},
data() {
let default_modules = this.all_modules;
let modules = this.get_customized_modules(default_modules, this.customization_settings);
return {
template_modules: template_modules,
modules: template_modules.slice(),
settings: {},
all_settings: {},
dragged_index: -1,
hovered_index: -1
};
},
methods: {
show_customize_dialog() {
if (!this.dialog) {
this.get_settings().then(() => {
const fields = this.make_fields();
this.make_and_show_dialog(fields);
});
} else {
this.dialog.show();
}
},
get_settings() {
return frappe.db.get_value("User", user, "home_settings").then(resp => {
this.all_settings = JSON.parse(resp.message["home_settings"]);
this.settings = this.all_settings[this.category];
});
},
make_fields() {
let fields = [];
let template_modules = this.template_modules;
let selected_modules = Object.keys(this.settings);
return {
default_modules: default_modules,
modules: modules,
new_settings: {},
dragged_index: -1,
hovered_index: -1,
}
},
methods: {
show_customize_dialog() {
if(!this.dialog) {
const fields = this.make_fields();
this.make_and_show_dialog(fields);
} else {
this.dialog.show();
}
},
make_fields() {
let fields = [];
this.modules.forEach(module => {
fields.push(this.get_module_select_field(module));
template_modules.forEach(module => {
fields.push(this.get_module_select_field(module, selected_modules));
if(module.links) {
fields.push(this.get_links_multiselect_field(module));
}
});
return fields;
},
make_and_show_dialog(fields) {
this.dialog = new frappe.ui.Dialog({
title: __("Customize " + this.category),
fields: fields,
primary_action_label: __("Update"),
primary_action: (values) => {
this.update_settings(values);
}
});
if (module.links) {
fields.push(this.get_links_multiselect_field(module));
}
});
this.dialog.modal_body.find('.clearfix').css({'display': 'none'});
this.dialog.modal_body.find('.frappe-control*[data-fieldtype="MultiSelect"]').css({'margin-bottom': '30px'});
return fields;
},
make_and_show_dialog(fields) {
this.dialog = new frappe.ui.Dialog({
title: __("Customize " + this.category),
fields: fields,
primary_action_label: __("Update"),
primary_action: values => {
let module_link_list_map = {};
this.dialog.show();
},
Object.keys(values).forEach(module_name => {
if (!module_name.includes("links") && values[module_name]) {
const links_str = values[module_name + "_links"] || "";
this.settings[module_name]["links"] = links_str;
if (values[module_name]) {
module_link_list_map[module_name] = {
links: links_str.split(","),
app: this.template_modules.filter(
m => m.module_name === module_name
)[0].app
};
}
}
});
update_settings(values) {
// Figure out the diff from the default settings known from modules
let new_settings = {};
const checkbox_fields = Object.keys(values).filter(f => !f.includes('links'));
frappe.db
.set_value("User", user, "home_settings", this.all_settings)
.then(resp => {
this.update_modules(module_link_list_map);
this.dialog.hide();
})
.fail(err => {
frappe.msgprint(err);
});
}
});
checkbox_fields.forEach(module_name => {
const default_module = this.default_modules.filter(f => f.module_name === module_name)[0];
this.dialog.modal_body.find(".clearfix").css({ display: "none" });
this.dialog.modal_body
.find('.frappe-control*[data-fieldtype="MultiSelect"]')
.css({ "margin-bottom": "30px" });
// Check if hidden changed
const default_hidden = default_module.hidden ? 1 : 0;
const new_hidden = !values[module_name] ? 1 : 0;
const hidden_changed = new_hidden != default_hidden;
this.dialog.show();
},
// Check if links changed
let links_changed = 0;
let new_links = [];
update_modules(module_link_list_map) {
frappe.call({
type: "GET",
method: "frappe.desk.moduleview.get_module_link_items_from_dict",
freeze: true,
args: {
module_link_list_map: module_link_list_map
},
callback: r => {
const module_links_dict = r.message;
this.template_modules.map((m, i) => {
let raw_links = module_links_dict[m.module_name];
raw_links.forEach(link => {
link.route = generate_route(link);
});
if (Object.keys(module_link_list_map).includes(m.module_name)) {
m.hidden = 0;
m.links = raw_links;
} else {
m.hidden = 1;
}
});
if(!new_hidden) {
const default_links = default_module.links.map(l => (l.name || l.label));
const new_links_str = values[module_name + '_links'] || '';
new_links = new_links_str ? new_links_str.split(",") : [];
links_changed = !this.are_arrays_equal(new_links, default_links);
}
this.modules = this.template_modules.filter(m => !m.hidden);
}
});
},
// Make new settings
let new_module_settings;
get_module_select_field(module, selected_modules) {
return {
label: __(module.module_name),
fieldname: module.module_name,
fieldtype: "Check",
default: selected_modules.includes(module.module_name) ? 1 : 0
};
},
if(hidden_changed || links_changed) {
new_module_settings = {};
if(hidden_changed) {
new_module_settings.hidden = new_hidden;
}
if(links_changed) {
new_module_settings.links = new_links;
}
}
get_links_multiselect_field(module) {
return {
label: __(""),
fieldname: module.module_name + "_links",
fieldtype: "MultiSelect",
get_data: function() {
let data = [];
if(new_module_settings) {
new_module_settings.app = this.default_modules.filter(m => m.module_name === module_name)[0].app;
new_settings[module_name] = new_module_settings;
}
});
frappe.call({
type: "GET",
method: "frappe.desk.moduleview.get_links",
async: false,
no_spinner: true,
args: {
app: module.app,
module: module.module_name
},
callback: function(r) {
data = r.message;
}
});
return data;
},
default: module.links.map(m => m.name || m.label),
depends_on: module.module_name
};
},
if(Object.keys(new_settings)) {
frappe.call({
type: "GET",
method:'frappe.desk.moduleview.update_desk_section_settings',
freeze: true,
args: {
desk_section: this.category,
new_settings: new_settings
},
callback: (r) => {
let new_settings_with_link_objects = r.message;
let home_settings = JSON.parse(frappe.boot.home_settings);
home_settings[this.category] = new_settings_with_link_objects;
frappe.boot.home_settings = JSON.stringify(home_settings);
box_dragstart(index) {
this.dragged_index = index;
},
this.modules = this.get_customized_modules(this.default_modules, new_settings_with_link_objects);
this.dialog.hide();
}
});
} else {
this.dialog.hide();
};
},
box_dragend(index) {
this.dragged_index = -1;
this.hovered_index = -1;
},
get_customized_modules(default_modules, customization_settings={}) {
return default_modules.map(module => {
let customized_module = JSON.parse(JSON.stringify(module));
box_enter(index) {
this.hovered_index = index;
},
const module_settings = customization_settings[module.module_name];
if(module_settings) {
if(module_settings.links) {
customized_module.links = module_settings.links;
}
customized_module.hidden = module_settings ? module_settings.hidden : 0;
}
box_drop(index) {
let d = this.dragged_index;
let h = this.hovered_index;
if (d < h) {
this.modules.splice(h, 0, this.modules[d]);
this.modules.splice(d, 1);
}
}
}
};
if(customized_module.links) {
customized_module.links.forEach(link => {
link.route = generate_route(link);
});
}
return customized_module;
});
},
get_module_select_field(module) {
return {
label: __(module.module_name),
fieldname: module.module_name,
fieldtype: "Check",
default: module.hidden ? 0 : 1
}
},
get_links_multiselect_field(module) {
return {
label: __(""),
fieldname: module.module_name + "_links",
fieldtype: "MultiSelect",
get_data: function() {
let data = [];
frappe.call({
type: "GET",
method:'frappe.desk.moduleview.get_links',
async: false,
no_spinner: true,
args: {
app: module.app,
module: module.module_name,
},
callback: function(r) {
data = r.message;
}
});
return data;
},
default: module.links.map(l => (l.name || l.label)),
depends_on: module.module_name
};
},
are_arrays_equal(arr1, arr2) {
if(arr1.length !== arr2.length) return false;
let areEqual = true;
arr1.map((d, i) => {
if(arr2[i] !== d) areEqual = false;
});
return areEqual;
},
box_dragstart(index) {
this.dragged_index = index;
},
box_dragend(index) {
this.dragged_index = -1;
this.hovered_index = -1;
},
box_enter(index) {
this.hovered_index = index;
},
box_drop(index) {
let d = this.dragged_index;
let h = this.hovered_index;
if (d < h) {
this.modules.splice(h, 0, this.modules[d]);
this.modules.splice(d, 1);
}
},
}
}
</script>
<style lang="less" scoped>

View file

@ -8,6 +8,7 @@
v-if="modules.filter(m => m.category === category).length"
:category="category"
:all_modules="modules.filter(m => m.category === category)"
:customization_settings="home_settings ? home_settings[category] : {}"
>
</desk-section>
@ -31,14 +32,38 @@ export default {
module.count = this.get_module_count(module.module_name);
});
const home_settings = frappe.boot.home_settings || '{}';
return {
route_str: frappe.get_route()[1],
module_label: '',
module_categories: ["Modules", "Domains", "Places", "Administration"],
modules: modules_list
modules: modules_list,
// // Desk customizations. Format of user settings:
// home_settings = { // <--- Settings
// "Domains": { // <--- Category (Desk Section)
// "Manufacturing": { // <--- Module
// "index": 3,
// "links": [],
// "hidden": 1,
// },
// },
// }
home_settings: JSON.parse(home_settings)
};
},
methods: {
get_settings() {
return frappe.db.get_value('User', user, 'home_settings')
.then(resp => {
this.all_settings = JSON.parse(resp.message['home_settings']);
this.settings = this.all_settings[this.category];
});
},
get_module_count(module_name) {
var module_doctypes = frappe.boot.notification_info.module_doctypes[module_name];
var sum = 0;

View file

@ -24,6 +24,8 @@ function generate_route(item) {
}
route = '#' + route;
} else {
route = item.route;
}
if(item.route_options) {

View file

@ -11,8 +11,7 @@ frappe.modules.Home = class {
}
make_body() {
this.$modules_container = this.$parent.find('.layout-main');
Vue.prototype.__ = window.__;
Vue.prototype.frappe = window.frappe;
new Vue({
el: this.$modules_container[0],
render: h => h(Modules)

View file

@ -48,8 +48,6 @@ frappe.views.pageview = {
let container = $('<div class="container"></div>').appendTo(page);
container = $('<div></div>').appendTo(container);
Vue.prototype.__ = window.__;
Vue.prototype.frappe = window.frappe;
new Vue({
el: container[0],
render: h => h(Desktop)

View file

@ -1,7 +0,0 @@
import Vue from 'vue/dist/vue.js';
if (!window.Vue) {
Vue.prototype.__ = window.__;
Vue.prototype.frappe = window.frappe;
window.Vue = Vue;
}

View file

@ -457,13 +457,23 @@ li.user-progress {
// like pop-over
.liked-by-popover {
min-width: 100px;
margin-top: -10px;
margin-bottom: -10px;
li {
margin: 15px 0px;
.popover-content {
padding: 0px;
overflow: scroll;
max-height: 150px;
}
min-width: 100px;
ul {
margin: 0px;
li {
padding: 10px;
cursor: pointer;
&:hover {
background: @btn-bg;
}
}
}
}
.screenshot {

View file

@ -11,9 +11,6 @@ body[data-route*="social"] {
}
.liked-by-popover {
font-size: @text-small;
li {
margin: 10px 0px;
}
}
.wall-container {
@ -56,6 +53,7 @@ body[data-route*="social"] {
min-height: 70px;
border: 1px solid @border-color;
border-radius: 4px;
overflow: hidden;
.content {
font-size: 14px;
img, iframe {

View file

@ -1,338 +1,343 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-09-25 11:39:04.533626",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-09-25 11:39:04.533626",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "content",
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Content",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "content",
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Content",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "comments",
"fieldtype": "Table",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "comments",
"length": 0,
"no_copy": 0,
"options": "Post Comment",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "comments",
"fieldtype": "Table",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "comments",
"length": 0,
"no_copy": 0,
"options": "Post Comment",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "liked_by",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Liked By",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "liked_by",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Liked By",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "is_pinned",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Pinned",
"length": 0,
"no_copy": 0,
"permlevel": 2,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "is_pinned",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Pinned",
"length": 0,
"no_copy": 0,
"permlevel": 2,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "is_globally_pinned",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Globally Pinned",
"length": 0,
"no_copy": 0,
"permlevel": 1,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "is_globally_pinned",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Globally Pinned",
"length": 0,
"no_copy": 0,
"permlevel": 1,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-11-01 10:08:33.902009",
"modified_by": "Administrator",
"module": "Social",
"name": "Post",
"name_case": "",
"owner": "Administrator",
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-03-11 16:32:20.638805",
"modified_by": "Administrator",
"module": "Social",
"name": "Post",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 0
},
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 0
},
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 1,
"import": 0,
"permlevel": 2,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 1,
"import": 0,
"permlevel": 2,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 2,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 2,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 0
},
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 1
}

View file

@ -65,4 +65,41 @@ def get_link_info(url):
def delete_post(post_name):
post = frappe.get_doc('Post', post_name)
post.delete()
frappe.publish_realtime('delete_post' + post_name, after_commit=True)
frappe.publish_realtime('delete_post' + post_name, after_commit=True)
def get_unseen_post_count():
post_count = frappe.db.count('Post')
view_post_count = get_viewed_posts(True)
return post_count - view_post_count
@frappe.whitelist()
def get_posts(filters=None, limit_start=0):
filters = frappe.utils.get_safe_filters(filters)
posts = frappe.get_list('Post',
fields= ['name', 'content', 'owner', 'creation', 'liked_by', 'is_pinned', 'is_globally_pinned'],
filters=filters,
limit_start=limit_start,
limit=20,
order_by= 'is_globally_pinned desc, creation desc')
viewed_posts = get_viewed_posts()
for post in posts:
post['seen'] = post.name in viewed_posts
return posts
def get_viewed_posts(only_count=False):
view_logs = frappe.db.get_all('View Log', filters={
'reference_doctype': 'Post',
'viewed_by': frappe.session.user
}, fields=['reference_name'])
return len(view_logs) if only_count else [log.reference_name for log in view_logs]
@frappe.whitelist()
def set_seen(post_name):
frappe.get_doc({
'doctype': 'View Log',
'reference_doctype': 'Post',
'reference_name': post_name,
'viewed_by': frappe.session.user
}).insert(ignore_permissions=True)

View file

@ -5,7 +5,16 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.core.doctype.user.user import extract_mentions
class PostComment(Document):
def after_insert(self):
mentions = extract_mentions(self.content)
for mention in mentions:
if mention == self.owner: continue
frappe.publish_realtime('mention', """{} mentioned you!
<br><a class="text-muted text-small" href="desk#social/home">Check Social<a>"""
.format(frappe.utils.get_fullname(self.owner)),
user=mention,
after_commit=True)
frappe.publish_realtime('new_post_comment' + self.parent, self, after_commit=True)

View file

@ -642,3 +642,16 @@ def gzip_decompress(data):
"""
with GzipFile(fileobj=io.BytesIO(data)) as f:
return f.read()
def get_safe_filters(filters):
try:
filters = json.loads(filters)
if isinstance(filters, (integer_types, float)):
filters = frappe.as_unicode(filters)
except (TypeError, ValueError):
# filters are not passed, not json
pass
return filters

View file

@ -191,7 +191,13 @@ def send_private_file(path):
response = Response(wrap_file(frappe.local.request.environ, f), direct_passthrough=True)
# no need for content disposition and force download. let browser handle its opening.
# response.headers.add(b'Content-Disposition', b'attachment', filename=filename.encode("utf-8"))
# Except for those that can be injected with scripts.
extension = os.path.splitext(path)[1]
blacklist = ['.svg', '.html', '.htm', '.xml']
if extension.lower() in blacklist:
response.headers.add(b'Content-Disposition', b'attachment', filename=filename.encode("utf-8"))
response.mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'