@@ -12,6 +12,8 @@ from frappe.utils.user import get_system_managers | |||
import frappe.permissions | |||
import frappe.share | |||
import re | |||
import json | |||
from frappe.limits import get_limits | |||
from frappe.website.utils import is_signup_enabled | |||
from frappe.utils.background_jobs import enqueue | |||
@@ -1086,4 +1088,12 @@ def generate_keys(user): | |||
user_details.save() | |||
return {"api_secret": api_secret} | |||
frappe.throw(frappe._("Not Permitted"), frappe.PermissionError) | |||
frappe.throw(frappe._("Not Permitted"), frappe.PermissionError) | |||
@frappe.whitelist() | |||
def update_profile_info(profile_info): | |||
profile_info = json.loads(profile_info) | |||
user = frappe.get_doc('User', frappe.session.user) | |||
user.update(profile_info) | |||
user.save() | |||
return user |
@@ -60,10 +60,6 @@ export default { | |||
this.show_preview = false; | |||
}) | |||
frappe.app_updates.on('user_image_updated', () => { | |||
this.$root.$emit('user_image_updated') | |||
}) | |||
this.update_primary_action(frappe.get_route()[1]) | |||
}, | |||
mounted() { | |||
@@ -22,13 +22,13 @@ | |||
</template> | |||
<script> | |||
export default { | |||
props: ['user'], | |||
props: ['user', 'from_date'], | |||
data() { | |||
return { | |||
history_logs: [], | |||
fetching: false, | |||
has_more_logs: true | |||
} | |||
}; | |||
}, | |||
created() { | |||
this.get_logs(); | |||
@@ -37,34 +37,37 @@ export default { | |||
get_logs() { | |||
this.fetching = true; | |||
const pull_limit = 10; | |||
frappe.db.get_list('Energy Point Log', { | |||
filters: { | |||
user: this.user, | |||
type: ['!=', 'Review'] | |||
}, | |||
fields: ['*'], | |||
limit: pull_limit, | |||
limit_start: this.history_logs.length | |||
}).then(data => { | |||
this.history_logs = this.history_logs.concat(data); | |||
this.has_more_logs = data.length === pull_limit; | |||
}).finally(() => { | |||
this.fetching = false; | |||
}) | |||
frappe.db | |||
.get_list('Energy Point Log', { | |||
filters: { | |||
user: this.user, | |||
type: ['!=', 'Review'], | |||
creation: ['>=', this.from_date] | |||
}, | |||
fields: ['*'], | |||
limit: pull_limit, | |||
limit_start: this.history_logs.length | |||
}) | |||
.then(data => { | |||
this.history_logs = this.history_logs.concat(data); | |||
this.has_more_logs = data.length === pull_limit; | |||
}) | |||
.finally(() => { | |||
this.fetching = false; | |||
}); | |||
} | |||
}, | |||
} | |||
} | |||
}; | |||
</script> | |||
<style lang="less"> | |||
@import "frappe/public/less/common"; | |||
@import 'frappe/public/less/common'; | |||
.log-list { | |||
padding: 15px; | |||
padding-left: 0px; | |||
position: relative; | |||
} | |||
.log-list:before { | |||
content: " "; | |||
content: ' '; | |||
border-left: 1px solid @border-color; | |||
position: absolute; | |||
top: 0px; | |||
@@ -82,7 +85,7 @@ export default { | |||
position: relative; | |||
} | |||
.history-log:before { | |||
content: " "; | |||
content: ' '; | |||
width: 7px; | |||
height: 7px; | |||
background-color: @border-color; | |||
@@ -2,8 +2,6 @@ | |||
<div ref="banner" class="banner" :style="background_style"> | |||
<div | |||
class="user-avatar container" | |||
:class="{'editable-image': is_own_profile}" | |||
@click="update_image" | |||
v-html="user_avatar"> | |||
</div> | |||
</div> | |||
@@ -13,29 +11,23 @@ export default { | |||
props: ['user_id'], | |||
data() { | |||
return { | |||
is_own_profile: this.user_id === frappe.session.user, | |||
user_avatar: frappe.avatar(this.user_id, 'avatar-xl'), | |||
background_style: { | |||
'background': '#262626' | |||
} | |||
user_banner: frappe.user_info(this.user_id).banner_image | |||
} | |||
}, | |||
created() { | |||
this.$root.$on('user_image_updated', () => { | |||
this.user_avatar = frappe.avatar(this.user_id, 'avatar-xl') | |||
this.user_banner = frappe.user_info(this.user_id).banner_image | |||
}) | |||
const user_banner = frappe.user_info(this.user_id).banner_image; | |||
if (user_banner) { | |||
this.background_style = { | |||
'background-image': `url('${user_banner}')` | |||
} | |||
} | |||
}, | |||
methods: { | |||
update_image() { | |||
if (this.is_own_profile) { | |||
frappe.social.update_user_image.show() | |||
computed: { | |||
background_style() { | |||
const style = {} | |||
if (this.user_banner) { | |||
style['background-image'] = `url('${this.user_banner}')` | |||
} | |||
return style; | |||
} | |||
}, | |||
} | |||
@@ -51,6 +43,7 @@ export default { | |||
background-size: cover; | |||
background-position: center; | |||
background-repeat: no-repeat; | |||
background-color: '#262626'; | |||
.user-avatar { | |||
position: relative; | |||
/deep/ .avatar { | |||
@@ -4,62 +4,126 @@ | |||
<h3>{{ user.fullname }}</h3> | |||
<p>{{ user.bio }}</p> | |||
<div class="location" v-if="user.location"> | |||
<span class="text-muted"><i class="fa fa-map-marker"> </i>{{ user.location }}</span> | |||
<span class="text-muted"> | |||
<i class="fa fa-map-marker"> </i> | |||
{{ user.location }} | |||
</span> | |||
</div> | |||
<div class="interest" v-if="user.interest"> | |||
<span class="text-muted"><i class="fa fa-puzzle-piece"> </i>{{ user.interest }}</span> | |||
<span class="text-muted"> | |||
<i class="fa fa-puzzle-piece"> </i> | |||
{{ user.interest }} | |||
</span> | |||
</div> | |||
<a v-if="show_add_info_link" @click="go_to_user_settings">{{ __('Add your information') }}</a> | |||
</div> | |||
<a class="home-link" @click="go_to_home()"> ← {{ __('Back To Home') }}</a> | |||
<a | |||
class="edit-profile-link" | |||
v-if="can_edit_profile" | |||
@click="edit_profile()" | |||
>{{ __('Edit Profile') }}</a> | |||
</div> | |||
</template> | |||
<script> | |||
export default { | |||
props: { | |||
'user_id': String, | |||
user_id: String | |||
}, | |||
data() { | |||
return { | |||
'user': frappe.user_info(this.user_id), | |||
'show_add_info_link': false | |||
} | |||
}, | |||
created() { | |||
if (frappe.social.is_session_user_page() && this.is_info_missing()) { | |||
this.show_add_info_link = true; | |||
} | |||
user: frappe.user_info(this.user_id), | |||
can_edit_profile: frappe.social.is_session_user_page() | |||
}; | |||
}, | |||
methods: { | |||
is_info_missing() { | |||
return !this.user.location || !this.user.interest || !this.user.bio; | |||
}, | |||
go_to_home() { | |||
frappe.set_route('social', 'home'); | |||
}, | |||
go_to_user_settings() { | |||
frappe.set_route('Form', 'User', this.user_id).then(()=> { | |||
frappe.dom.scroll_to_section('More Information'); | |||
}) | |||
edit_profile() { | |||
const edit_profile_dialog = new frappe.ui.Dialog({ | |||
title: __('Edit Profile'), | |||
fields: [ | |||
{ | |||
fieldtype: 'Attach Image', | |||
fieldname: 'user_image', | |||
label: 'Profile Image', | |||
reqd: 1 | |||
}, | |||
{ | |||
fieldtype: 'Data', | |||
fieldname: 'interest', | |||
label: 'Interests', | |||
reqd: 1 | |||
}, | |||
{ | |||
fieldtype: 'Column Break' | |||
}, | |||
{ | |||
fieldtype: 'Attach Image', | |||
fieldname: 'banner_image', | |||
label: 'Banner Image', | |||
reqd: 1 | |||
}, | |||
{ | |||
fieldtype: 'Data', | |||
fieldname: 'location', | |||
label: 'Location', | |||
reqd: 1 | |||
}, | |||
{ | |||
fieldtype: 'Section Break', | |||
fieldname: 'Interest' | |||
}, | |||
{ | |||
fieldtype: 'Small Text', | |||
fieldname: 'bio', | |||
label: 'Bio', | |||
reqd: 1 | |||
} | |||
], | |||
primary_action: values => { | |||
edit_profile_dialog.disable_primary_action(); | |||
frappe | |||
.xcall('frappe.core.doctype.user.user.update_profile_info', { | |||
profile_info: values | |||
}) | |||
.then(user => { | |||
user.image = user.user_image; | |||
let user_info = frappe.user_info(this.user_id); | |||
this.user = Object.assign(user_info, user); | |||
this.$root.$emit('user_image_updated'); | |||
edit_profile_dialog.hide(); | |||
}) | |||
.finally(() => { | |||
edit_profile_dialog.enable_primary_action(); | |||
}); | |||
}, | |||
primary_action_label: __('Save') | |||
}); | |||
edit_profile_dialog.set_values({ | |||
user_image: this.user.image, | |||
banner_image: this.user.banner_image, | |||
location: this.user.location, | |||
interest: this.user.interest, | |||
bio: this.user.bio | |||
}); | |||
edit_profile_dialog.show(); | |||
} | |||
} | |||
} | |||
}; | |||
</script> | |||
<style lang="less" scoped> | |||
.profile-sidebar { | |||
padding: 10px 10px 0 0 | |||
padding: 10px 10px 0 0; | |||
} | |||
.user-details { | |||
min-height: 150px; | |||
.location, .interest { | |||
.location, | |||
.interest { | |||
margin-bottom: 10px; | |||
i { | |||
width: 15px; | |||
} | |||
} | |||
.home-link { | |||
margin-top: 15px; | |||
} | |||
} | |||
.edit-profile-link { | |||
margin-top: 15px; | |||
} | |||
</style> |
@@ -3,47 +3,53 @@ | |||
<ul class="list-unstyled user-list"> | |||
<li class="user-card user-list-header text-medium"> | |||
<span class="user-details text-muted"> | |||
{{ __('User') }} | |||
<input | |||
class="form-control" | |||
type="search" | |||
placeholder="Search User" | |||
v-model="filter_users_by" | |||
> | |||
</span> | |||
<span class="user-points text-muted"> | |||
{{ __('Energy Points') }} | |||
</span> | |||
<span class="user-points text-muted"> | |||
{{ __('Review Points') }} | |||
</span> | |||
<span class="user-points text-muted"> | |||
{{ __('Points Given') }} | |||
<span class="flex-40"></span> | |||
<span class="flex-20 text-muted"> | |||
<select class="form-control" data-toggle="tooltip" title="Period" v-model="period"> | |||
<option v-for="value in period_options" :key="value" :value="value">{{ value }}</option> | |||
</select> | |||
</span> | |||
</li> | |||
<li | |||
v-for="user in filtered_users" | |||
:key="user.name"> | |||
<li class="user-card user-list-header text-medium"> | |||
<span class="user-details text-muted">{{ __('User') }}</span> | |||
<span | |||
class="flex-20 text-muted" | |||
v-for="title in ['Energy Points', 'Review Points', 'Points Given']" | |||
:key="title" | |||
>{{ __(title) }}</span> | |||
</li> | |||
<li v-for="user in filtered_users" :key="user.name"> | |||
<div class="user-card" @click="toggle_log(user.name)"> | |||
<div class="user-details flex"> | |||
<span class="user-details flex"> | |||
<span v-html="get_avatar(user.name)"></span> | |||
<span> | |||
<a @click="go_to_profile_page(user.name)">{{ user.fullname }}</a> | |||
<div class="text-muted text-medium" :class="{'italic': !user.bio}"> | |||
{{ frappe.ellipsis(user.bio, 100) || 'No Bio'}} | |||
</div> | |||
<div | |||
class="text-muted text-medium" | |||
:class="{'italic': !user.bio}" | |||
>{{ frappe.ellipsis(user.bio, 100) || 'No Bio'}}</div> | |||
</span> | |||
</div> | |||
<span class="text-muted text-nowrap user-points"> | |||
{{ user.energy_points }} | |||
</span> | |||
<span class="text-muted text-nowrap user-points"> | |||
{{ user.review_points }} | |||
</span> | |||
<span class="text-muted text-nowrap user-points"> | |||
{{ user.given_points }} | |||
</span> | |||
<span | |||
class="text-muted text-nowrap flex-20" | |||
v-for="key in ['energy_points', 'review_points', 'given_points']" | |||
:key="key" | |||
>{{ user[key] }}</span> | |||
</div> | |||
<energy-point-history | |||
v-show="show_log_for===user.name" | |||
class="energy-point-history" | |||
:user="user.name" | |||
:key="user.name + user.energy_points"> | |||
</energy-point-history> | |||
:from_date="from_date" | |||
:key="user.name + user.energy_points" | |||
></energy-point-history> | |||
</li> | |||
<li class="user-card text-muted" v-if="!filtered_users.length">{{__('No user found')}}</li> | |||
</ul> | |||
@@ -61,17 +67,33 @@ export default { | |||
filter_users_by: null, | |||
sort_users_by: 'energy_points', | |||
sort_order: 'desc', | |||
show_log_for: null | |||
} | |||
show_log_for: null, | |||
period_options: ['Lifetime', 'Last Month', 'Last Week', 'Today'], | |||
period: 'Lifetime' | |||
}; | |||
}, | |||
computed: { | |||
from_date() { | |||
const days_to_deduct = { | |||
'Last Week': 7, | |||
'Last Month': 30 | |||
}; | |||
if (this.period === 'Lifetime') { | |||
return null; | |||
} | |||
if (this.period === 'Today') { | |||
return frappe.datetime.get_today(); | |||
} | |||
return frappe.datetime.add_days(moment(), -days_to_deduct[this.period]); | |||
}, | |||
filtered_users() { | |||
let filtered = this.users.slice(); | |||
if (this.filter_users_by) { | |||
filtered = filtered.filter(user => | |||
user.fullname.toLowerCase().includes(this.filter_users_by.toLowerCase()) | |||
) | |||
user.fullname | |||
.toLowerCase() | |||
.includes(this.filter_users_by.toLowerCase()) | |||
); | |||
} | |||
if (this.sort_users_by) { | |||
@@ -89,15 +111,23 @@ export default { | |||
} | |||
if (this.sort_order === 'desc') { | |||
return_value = -return_value | |||
return_value = -return_value; | |||
} | |||
return return_value | |||
return return_value; | |||
}); | |||
} | |||
return filtered; | |||
} | |||
}, | |||
watch: { | |||
period() { | |||
this.fetch_users_energy_points_and_update_users(); | |||
} | |||
}, | |||
mounted() { | |||
$('[data-toggle="tooltip"]').tooltip(); | |||
}, | |||
created() { | |||
const standard_users = ['Administrator', 'Guest', 'guest@example.com']; | |||
this.users = frappe.boot.user_info; | |||
@@ -111,37 +141,42 @@ export default { | |||
}, | |||
methods: { | |||
get_avatar(user) { | |||
return frappe.avatar(user, 'avatar-medium') | |||
return frappe.avatar(user, 'avatar-medium'); | |||
}, | |||
go_to_profile_page(user) { | |||
frappe.set_route('social', 'profile', user) | |||
frappe.set_route('social', 'profile', user); | |||
}, | |||
fetch_users_energy_points_and_update_users() { | |||
frappe.xcall( | |||
'frappe.social.doctype.energy_point_log.energy_point_log.get_user_energy_and_review_points' | |||
).then(data => { | |||
let users = this.users.slice(); | |||
this.users = users.map(user => { | |||
const points = data[user.name] || {}; | |||
user.energy_points = points.energy_points || 0; | |||
user.review_points = points.review_points || 0; | |||
user.given_points = points.given_points || 0; | |||
return user; | |||
frappe | |||
.xcall( | |||
'frappe.social.doctype.energy_point_log.energy_point_log.get_user_energy_and_review_points', | |||
{ | |||
from_date: this.from_date | |||
} | |||
) | |||
.then(data => { | |||
let users = this.users.slice(); | |||
this.users = users.map(user => { | |||
const points = data[user.name] || {}; | |||
user.energy_points = points.energy_points || 0; | |||
user.review_points = points.review_points || 0; | |||
user.given_points = points.given_points || 0; | |||
return user; | |||
}); | |||
}); | |||
}); | |||
}, | |||
toggle_log(user) { | |||
if (this.show_log_for === user) { | |||
this.show_log_for = null | |||
this.show_log_for = null; | |||
} else { | |||
this.show_log_for = user | |||
this.show_log_for = user; | |||
} | |||
} | |||
} | |||
} | |||
}; | |||
</script> | |||
<style lang="less" scoped> | |||
@import "frappe/public/less/variables"; | |||
@import 'frappe/public/less/variables'; | |||
.user-list { | |||
border-left: 1px solid @border-color; | |||
border-right: 1px solid @border-color; | |||
@@ -158,11 +193,14 @@ export default { | |||
} | |||
} | |||
} | |||
.user-points { | |||
.flex-20 { | |||
flex: 0 0 20%; | |||
text-align: right; | |||
align-self: center; | |||
} | |||
.flex-40 { | |||
flex: 0 0 40%; | |||
} | |||
.user-list-header { | |||
background-color: @light-bg; | |||
} | |||
@@ -48,33 +48,6 @@ frappe.social.post_dialog = new frappe.ui.Dialog({ | |||
} | |||
}); | |||
frappe.social.update_user_image = new frappe.ui.Dialog({ | |||
title: __("User Image"), | |||
fields: [ | |||
{ | |||
fieldtype: "Attach Image", | |||
fieldname: "image", | |||
label: __("Image"), | |||
reqd: 1, | |||
default: frappe.user.image() | |||
}, | |||
], | |||
primary_action_label: __('Set Image'), | |||
primary_action: (values) => { | |||
const user = frappe.session.user; | |||
frappe.db.set_value('User', user, 'user_image', values.image) | |||
.then((resp) => { | |||
frappe.boot.user_info[user].image = resp.message.user_image; | |||
frappe.app_updates.trigger('user_image_updated'); | |||
frappe.social.update_user_image.clear(); | |||
frappe.social.update_user_image.hide(); | |||
}) | |||
.fail((err) => { | |||
frappe.msgprint(err); | |||
}); | |||
} | |||
}); | |||
frappe.social.is_home_page = () => { | |||
return frappe.get_route()[0] === 'social' && frappe.get_route()[1] === 'home'; | |||
}; | |||
@@ -1,13 +1,13 @@ | |||
<div class="page-head"> | |||
<div class="page-head flex align-center"> | |||
<div class="container"> | |||
<div class="row"> | |||
<div class="row flex align-center"> | |||
<div class="col-md-7 col-sm-8 col-xs-6 page-title"> | |||
<!-- title --> | |||
<h1> | |||
<h1 class="flex" style="margin: auto;"> | |||
<div class="title-image hide hidden-md hidden-lg"> | |||
</div> | |||
<div class="ellipsis title-text"></div> | |||
<span class="indicator hide"></span> | |||
<span class="indicator whitespace-nowrap hide"></span> | |||
</h1> | |||
</div> | |||
<div class="text-right col-md-5 col-sm-4 col-xs-6 page-actions"> | |||
@@ -138,7 +138,7 @@ frappe.ui.Page = Class.extend({ | |||
}, | |||
clear_indicator: function() { | |||
return this.indicator.removeClass().addClass("indicator hide"); | |||
return this.indicator.removeClass().addClass("indicator whitespace-nowrap hide"); | |||
}, | |||
get_icon_label: function(icon, label) { | |||
@@ -1054,6 +1054,12 @@ img.img-loading:after { | |||
content: "\f00b"; | |||
} | |||
// utilities | |||
.whitespace-nowrap { | |||
white-space: nowrap; | |||
} | |||
// popover | |||
.popover { | |||
border-radius: 4px; | |||
@@ -1070,3 +1076,9 @@ body.full-width { | |||
} | |||
} | |||
} | |||
// utilities | |||
.whitespace-nowrap { | |||
white-space: nowrap; | |||
} |