From 413b187a1b230187a09b9f90b7bc88956b205ed0 Mon Sep 17 00:00:00 2001 From: Prssanna Desai Date: Tue, 30 Jul 2019 15:41:20 +0530 Subject: [PATCH 01/41] feat(User Profile): Add new user profiles with energy points information --- frappe/core/doctype/user/user.py | 10 +- frappe/core/page/dashboard/dashboard.js | 3 +- frappe/core/page/dashboard/dashboard.py | 14 +- .../dashboard_chart/dashboard_chart.py | 6 +- .../desk/page/user_profile/user_profile.css | 104 +++++ .../desk/page/user_profile/user_profile.html | 63 +++ frappe/desk/page/user_profile/user_profile.js | 392 ++++++++++++++++++ .../desk/page/user_profile/user_profile.json | 22 + frappe/desk/page/user_profile/user_profile.py | 64 +++ .../user_profile/user_profile_sidebar.html | 25 ++ .../public/js/frappe/ui/toolbar/navbar.html | 2 +- 11 files changed, 686 insertions(+), 19 deletions(-) create mode 100644 frappe/desk/page/user_profile/user_profile.css create mode 100644 frappe/desk/page/user_profile/user_profile.html create mode 100644 frappe/desk/page/user_profile/user_profile.js create mode 100644 frappe/desk/page/user_profile/user_profile.json create mode 100644 frappe/desk/page/user_profile/user_profile.py create mode 100644 frappe/desk/page/user_profile/user_profile_sidebar.html diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 3b2f6a9604..73c4dab3d1 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1068,12 +1068,4 @@ def generate_keys(user): user_details.save() return {"api_secret": api_secret} - 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 \ No newline at end of file + frappe.throw(frappe._("Not Permitted"), frappe.PermissionError) \ No newline at end of file diff --git a/frappe/core/page/dashboard/dashboard.js b/frappe/core/page/dashboard/dashboard.js index ebaf8615d4..26cddd90ce 100644 --- a/frappe/core/page/dashboard/dashboard.js +++ b/frappe/core/page/dashboard/dashboard.js @@ -214,7 +214,8 @@ class DashboardChart { return frappe.xcall( method, { - chart_name: this.chart_doc.name, + chart: this.chart_doc, + cache: 1, filters: filters, refresh: refresh ? 1 : 0, } diff --git a/frappe/core/page/dashboard/dashboard.py b/frappe/core/page/dashboard/dashboard.py index 552299f2ac..41c81d587d 100644 --- a/frappe/core/page/dashboard/dashboard.py +++ b/frappe/core/page/dashboard/dashboard.py @@ -8,21 +8,25 @@ from frappe.utils import add_to_date def cache_source(function): def wrapper(*args, **kwargs): - chart_name = kwargs.get("chart_name") + chart = kwargs.get("chart") + cache = kwargs.get("cache") + if not int(cache): + return function(chart, cache) + chart_name = frappe.parse_json(chart)['name'] cache_key = 'chart-data:{}'.format(chart_name) if int(kwargs.get("refresh") or 0): - results = generate_and_cache_results(chart_name, function, cache_key) + results = generate_and_cache_results(chart, cache, chart_name, function, cache_key) else: cached_results = frappe.cache().get_value(cache_key) if cached_results: results = json.loads(frappe.safe_decode(cached_results)) else: - results = generate_and_cache_results(chart_name, function, cache_key) + results = generate_and_cache_results(chart, cache, chart_name, function, cache_key) return results return wrapper -def generate_and_cache_results(chart_name, function, cache_key): - results = function(chart_name) +def generate_and_cache_results(chart, cache, chart_name, function, cache_key): + results = function(chart, cache) frappe.cache().set_value(cache_key, json.dumps(results, default=str)) frappe.db.set_value("Dashboard Chart", chart_name, "last_synced_on", frappe.utils.now(), update_modified = False) return results diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 0273fe80b1..f43b6d9dae 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -11,12 +11,12 @@ from frappe.model.document import Document @frappe.whitelist() @cache_source -def get(chart_name, from_date=None, to_date=None, refresh = None): - chart = frappe.get_doc('Dashboard Chart', chart_name) +def get(chart, cache, from_date=None, to_date=None, refresh = None): + chart = frappe.parse_json(chart) timespan = chart.timespan timegrain = chart.time_interval - filters = json.loads(chart.filters_json) + filters = json.loads(chart['filters_json']) # don't include cancelled documents filters['docstatus'] = ('<', 2) diff --git a/frappe/desk/page/user_profile/user_profile.css b/frappe/desk/page/user_profile/user_profile.css new file mode 100644 index 0000000000..16c9b9fd49 --- /dev/null +++ b/frappe/desk/page/user_profile/user_profile.css @@ -0,0 +1,104 @@ +.user-image-container { + margin-top: 7px; + padding-bottom: 100%; +} + +.standard-image { + font-size: 72px; +} + +.profile-details { + margin: -5px 5px; +} + +.profile-links { + margin: 30px 5px; +} + +.user-initial { + font-size: 72px; +} + +.chart-column-container{ + border: 1px solid #d1d8dd; + border-radius: 4px; + margin: 30px 0; + padding: 5px 15px; +} + +.heatmap-container { + height: 180px +} +.performance-heatmap .chart-container { + margin: 0 auto; +} + +.performance-heatmap .frappe-chart .chart-legend { + display: none; +} + +.percentage-chart-container { + height: 170px; +} +/* .performance-graphs .chart-column-container { + width: 95%; +} */ + +.performance-graphs { + margin: 15px 30px; +} + +.recent-activity { + margin: 30px; + font-size: 12px; +} + +.show-more-activity { + text-align: center; + margin-top: 30px; +} + +.positive-points { + color: #45A163; +} + +.negative-points { + color: #e42121; +} + +.points-reason { + display: flow-root; +} + +.points-update { + float: left; + text-align: right; + margin-right: 15px; + width: 3%; +} + +.recent-points-item { + margin: 15px; +} + +.interest-icon { + margin-right: 5px; +} + +.chart-filter { + position: relative; + left: 10px; + top: 5px; + margin-right: 5px; + z-index: 1; +} + +.filter-label { + margin-right: 4px; +} + +.performance-title { + position: relative; + left: 30px; + top: 20px; +} \ No newline at end of file diff --git a/frappe/desk/page/user_profile/user_profile.html b/frappe/desk/page/user_profile/user_profile.html new file mode 100644 index 0000000000..5470d7515a --- /dev/null +++ b/frappe/desk/page/user_profile/user_profile.html @@ -0,0 +1,63 @@ +
+
+
Activity Overview
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+
+

Recent Activity

+
+
+ +
+
+
+ diff --git a/frappe/desk/page/user_profile/user_profile.js b/frappe/desk/page/user_profile/user_profile.js new file mode 100644 index 0000000000..67a9deae89 --- /dev/null +++ b/frappe/desk/page/user_profile/user_profile.js @@ -0,0 +1,392 @@ +frappe.pages['user-profile'].on_page_load = function(wrapper) { + + let page = frappe.ui.make_app_page({ + parent: wrapper, + title: __("User Profile"), + }); + + let user_profile = new UserProfile(wrapper) + $(wrapper).bind('show',()=> { + user_profile.show(); + }); +} + +class UserProfile { + + constructor(wrapper) { + this.wrapper = $(wrapper); + this.page = wrapper.page; + this.sidebar = this.wrapper.find(".layout-side-section"); + this.main_section = this.wrapper.find(".layout-main-section"); + } + + show() { + this.route = frappe.get_route(); + //validate if user + if (this.route.length > 1) { + let user_id = this.route.slice(-1)[0]; + this.check_user_exists(user_id); + } else { + this.user_id = frappe.session.user; + this.make_user_profile(); + } + } + + check_user_exists(user) { + frappe.db.exists('User', user).then( exists => { + if(!exists) { + frappe.msgprint('User does not exist'); + } else { + this.user_id = user; + this.make_user_profile(); + } + }) + } + + make_user_profile() { + frappe.set_route('user-profile', this.user_id); + this.user = frappe.user_info(this.user_id); + this.page.set_title(this.user.fullname); + this.setup_user_search(); + this.main_section.empty().append(frappe.render_template('user_profile')); + this.energy_points = 0; + this.rank = 0; + this.render_user_details(); + this.render_heatmap(); + this.render_years_filter_dropdown(); + this.render_line_chart(); + this.render_percentage_chart('type', 'Type Distribution'); + this.filter_charts(); + this.setup_show_more_activity(); + this.render_user_activity(); + } + + setup_user_search() { + var me = this; + this.$user_search_button = this.page.set_secondary_action('Change User', function() { + me.show_user_search_dialog() + }); + } + + show_user_search_dialog() { + let dialog = new frappe.ui.Dialog({ + title: __('Change User'), + fields: [ + { + fieldtype: 'Link', + fieldname: 'user', + options: 'User', + label: __('User'), + } + ], + primary_action_label: __('Go'), + primary_action: ({ user }) => { + dialog.hide(); + this.check_user_exists(user); + } + }); + dialog.show(); + } + + render_heatmap() { + this.heatmap = new frappe.Chart(".performance-heatmap", { + type: 'heatmap', + title: "Energy Points Monthly Distribution", + countLabel: 'Level', + data:{}, + discreteDomains: 0, + colors: ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e'], + }); + this.update_heatmap_data(); + } + + update_heatmap_data(date_from) { + frappe.xcall('frappe.desk.page.user_profile.user_profile.get_energy_points_heatmap_data', { + user: this.user_id, + date: date_from || frappe.datetime.year_start() + }).then((r)=> { + this.heatmap.update({dataPoints:r}); + }); + } + + render_years_filter_dropdown() { + this.user_creation = frappe.boot.user.creation; + let creation_year = this.get_year(this.user_creation); + this.year_dropdown = this.wrapper.find('.year-dropdown'); + let dropdown_html = ''; + let current_year = this.get_year(frappe.datetime.now_date()); + for(var year = current_year; year >= creation_year; year--) { + dropdown_html+=`
  • ${__(year)}
  • ` + } + this.year_dropdown.html(dropdown_html); + } + + get_year(date_str) { + return date_str.substring(0,date_str.indexOf('-')); + } + + render_line_chart() { + this.line_chart_filters = {'user': this.user_id}; + this.line_chart_data = { + timespan: 'Last Month', + time_interval: 'Daily', + type:'Line', + value_based_on: "points", + chart_type: "Sum", + document_type: "Energy Point Log", + name: 'Energy Points', + width: 'half', + based_on: 'creation' + } + this.line_chart = new frappe.Chart( ".performance-line-chart", { // or DOM element + title: 'Energy Points', + type: 'line', + height: 200, + data: { + labels: [], + datasets: [{}] + }, + colors: ['purple'], + axisOptions: { + xIsSeries: 1 + } + }); + this.update_line_chart_data(); + } + + update_line_chart_data() { + this.line_chart_data.filters_json = JSON.stringify(this.line_chart_filters); + frappe.xcall('frappe.desk.doctype.dashboard_chart.dashboard_chart.get', { + chart: this.line_chart_data, + cache: 0, + }).then(chart=> { + this.line_chart.update(chart); + }); + } + + render_percentage_chart(field, title) { + frappe.xcall('frappe.desk.page.user_profile.user_profile.get_energy_points_pie_chart_data', { + user: this.user_id, + field: field + }).then(chart=> { + if(chart.labels.length) { + this.percentage_chart = new frappe.Chart( '.performance-percentage-chart', { // or DOM element + title: title, + type: 'percentage', + data: { + labels: chart.labels, + datasets: chart.datasets + }, + barOptions: { + height: 11, + depth: 1 + }, + height: 160, + maxSlices: 8, + colors: ['#5e64ff', '#743ee2', '#ff5858', '#ffa00a', '#feef72', '#28a745', '#98d85b', '#a9a7ac'], + }); + } else { + $('.percentage-chart-container').hide(); + } + }); + } + + + filter_charts() { + this.year_dropdown.on('click','li a',(e)=> { + let selected_year = e.currentTarget.textContent; + this.wrapper.find('.year-filter .filter-label').text(selected_year); + this.update_heatmap_data(frappe.datetime.obj_to_str(selected_year)); + }); + + this.period_dropdown = this.wrapper.find('.period-dropdown').on('click','li a',(e)=> { + let selected_period = e.currentTarget.textContent; + this.line_chart_data.timespan = selected_period; + this.wrapper.find('.period-filter .filter-label').text(selected_period); + this.update_line_chart_data(); + }); + + this.type_dropdown = this.wrapper.find('.type-dropdown').on('click','li a',(e)=> { + let selected_type = e.currentTarget.textContent; + if(selected_type === 'All') delete this.line_chart_filters.type; + else this.line_chart_filters.type = selected_type; + this.wrapper.find('.type-filter .filter-label').text(selected_type); + this.update_line_chart_data(); + }); + + this.field_dropdown = this.wrapper.find('.field-dropdown').on('click','li a',(e)=> { + let selected_field = e.currentTarget.textContent; + let fieldname = $(e.currentTarget).attr('data-fieldname') + this.wrapper.find('.field-filter .filter-label').text(selected_field); + let title = selected_field + ' Distribution'; + this.render_percentage_chart(fieldname, title); + }); + } + + edit_profile() { + const edit_profile_dialog = new frappe.ui.Dialog({ + title: __('Edit Profile'), + fields: [ + { + fieldtype: 'Attach Image', + fieldname: 'user_image', + label: 'Profile Image', + }, + { + fieldtype: 'Data', + fieldname: 'interest', + label: 'Interests', + }, + { + fieldtype: 'Column Break' + }, + { + fieldtype: 'Data', + fieldname: 'location', + label: 'Location', + }, + { + fieldtype: 'Section Break', + fieldname: 'Interest', + }, + { + fieldtype: 'Small Text', + fieldname: 'bio', + label: 'Bio', + } + ], + primary_action: values => { + edit_profile_dialog.disable_primary_action(); + frappe.xcall('frappe.desk.page.user_profile.user_profile.update_profile_info', { + profile_info: values + }) + .then(user => { + user.image = user.user_image; + this.user = Object.assign(values, user); + edit_profile_dialog.hide(); + this.render_user_details(); + }) + .finally(() => { + edit_profile_dialog.enable_primary_action(); + }); + }, + primary_action_label: __('Save') + }); + edit_profile_dialog.set_values({ + user_image: this.user.image, + location: this.user.location, + interest: this.user.interest, + bio: this.user.bio + }); + edit_profile_dialog.show(); + } + + render_user_details() { + this.get_user_energy_points_and_rank().then(()=> { + this.sidebar.empty().append(frappe.render_template('user_profile_sidebar', { + user_image: frappe.avatar(this.user_id,'avatar-frame', 'user_image', this.user.image), + user_abbr: this.user.abbr, + user_location: this.user.location, + user_interest: this.user.interest, + user_bio: this.user.bio, + energy_points: this.energy_points, + rank: this.rank + })); + if(this.user_id !== frappe.session.user) { + this.wrapper.find('.profile-links').hide(); + } else { + this.wrapper.find(".edit-profile-link").on("click", () => { + this.edit_profile(); + }); + this.wrapper.find(".user-settings-link").on("click", () => { + this.go_to_user_settings(); + }); + } + }); + } + + get_user_energy_points_and_rank() { + return frappe.xcall('frappe.desk.page.user_profile.user_profile.get_user_points_and_rank', { + user: this.user_id + }) + .then(user => { + if(user[0]) { + let user_info = user[0]; + console.log(user_info); + this.energy_points = user_info[1]; + this.rank = user_info[2]; + } + }) + } + + go_to_user_settings() { + frappe.set_route('Form', 'User', this.user_id); + } + + render_user_activity(append) { + this.$recent_activity_list = $('.recent-activity-list'); + let get_recent_energy_points_html = (field) => { + let points_html= field.type === 'Auto' || field.type === 'Appreciation' + ? `
    +${__(field.points)}
    ` + : `
    ${__(field.points)}
    `; + let message_html = this.get_message_html(field); + + return `

    + ${points_html} + + ${message_html} + +

    `; + } + frappe.xcall('frappe.desk.page.user_profile.user_profile.get_energy_points_list', { + start:this.activity_start, + limit: this.activity_end, + user: this.user_id + }).then(list=> { + if(!list.length) { + this.wrapper.find('.show-more-activity a').html('No More Activity'); + } + let html = list.map(get_recent_energy_points_html).join(''); + if(append) this.$recent_activity_list.append(html); + else this.$recent_activity_list.html(html); + }) + } + + get_message_html(field) { + let owner_name = frappe.user.full_name(field.owner).trim(); + let doc_link = frappe.utils.get_form_link(field.reference_doctype, field.reference_name); + let message_html = ''; + if(field.type === 'Auto' ) { + message_html = `For ${__(field.rule)} ${__(field.reference_name)}`; + } else { + let user_str = this.user_id === frappe.session.user ? 'your': frappe.user.full_name(field.user) + "'s"; + let message; + if(field.type === 'Appreciation') { + message = `${__(owner_name)} appreciated ${__(user_str)} work on `; + } else if(field.type === 'Criticism') { + message = `${__(owner_name)} criticized ${__(user_str)} work on ` + } else if(field.type === 'Revert') { + message = `${__(owner_name)} reverted ${__(user_str)} points on `; + } + message_html = `${message}${__(field.reference_name)} + `; + } + return message_html; + } + + setup_show_more_activity() { + this.activity_start = 0; + this.activity_end = 10; + this.wrapper.find('.show-more-activity').on('click', ()=>this.show_more_activity()); + } + + show_more_activity() { + this.activity_start = this.activity_end; + this.activity_end+=10; + this.render_user_activity(true); + } + +} + + + diff --git a/frappe/desk/page/user_profile/user_profile.json b/frappe/desk/page/user_profile/user_profile.json new file mode 100644 index 0000000000..1fb0085eb2 --- /dev/null +++ b/frappe/desk/page/user_profile/user_profile.json @@ -0,0 +1,22 @@ +{ + "content": null, + "creation": "2019-07-22 12:23:38.425877", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2019-07-22 12:23:38.425877", + "modified_by": "Administrator", + "module": "Desk", + "name": "user-profile", + "owner": "Administrator", + "page_name": "User Profile", + "roles": [ + { + "role": "All" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0 +} \ No newline at end of file diff --git a/frappe/desk/page/user_profile/user_profile.py b/frappe/desk/page/user_profile/user_profile.py new file mode 100644 index 0000000000..c01492c20b --- /dev/null +++ b/frappe/desk/page/user_profile/user_profile.py @@ -0,0 +1,64 @@ +import frappe + +@frappe.whitelist() +def get_energy_points_heatmap_data(user, date): + return dict(frappe.db.sql("""select unix_timestamp(date(creation)), sum(points) + from `tabEnergy Point Log` + where + date(creation) > subdate('{date}', interval 1 year) and + date(creation) < subdate('{date}', interval -1 year) and + user = '{user}' and + type != 'Review' + group by date(creation) + order by creation asc""".format(user=user, date = date))) + + +@frappe.whitelist() +def get_energy_points_pie_chart_data(user, field): + result = (frappe.db.sql("""select {field}, ABS(sum(points)) + from `tabEnergy Point Log` + where + user = '{user}' and + type != 'Review' + group by {field} + order by {field}""".format(user=user, field=field))) + # result = frappe.db.get_all('Energy Point Log', filters={'user': user, 'type': ['!=', 'Review']}, group_by='type', order_by = 'type', fields=['type', 'sum(points) as points'], as_list = True) + print(result) + return { + "labels": [r[0] for r in result if r[0]!=None], + "datasets": [{ + "values": [r[1] for r in result] + }] + } + +@frappe.whitelist() +def get_user_points_and_rank(user, date=None): + result = frappe.db.sql("""select user, sum(points) as points, rank() over (order by points desc) as rank + from `tabEnergy Point Log` + where creation > '{date}' + group by user""".format(date=date)) + return [r for r in result if r[0]==user] + + +@frappe.whitelist() +def update_profile_info(profile_info): + profile_info = frappe.parse_json(profile_info) + #for loop + if 'location' not in profile_info: + profile_info['location'] = None + if 'interest' not in profile_info: + profile_info['interest'] = None + if 'user_image' not in profile_info: + profile_info['user_image'] = None + if 'bio' not in profile_info: + profile_info['bio'] = None + user = frappe.get_doc('User', frappe.session.user) + user.update(profile_info) + user.save() + return user + +@frappe.whitelist() +def get_energy_points_list(start, limit, user): + return frappe.db.get_list('Energy Point Log',filters = {'user': user, 'type': ['!=', 'Review']}, + fields=['name','user', 'points', 'reference_doctype', 'reference_name', 'reason', 'type', 'seen', 'rule', 'owner', 'creation'], + start=start, limit=limit, order_by='creation desc') diff --git a/frappe/desk/page/user_profile/user_profile_sidebar.html b/frappe/desk/page/user_profile/user_profile_sidebar.html new file mode 100644 index 0000000000..d9926a60c5 --- /dev/null +++ b/frappe/desk/page/user_profile/user_profile_sidebar.html @@ -0,0 +1,25 @@ +
    +
    + {% if user_image %} + {{user_image}} + {% endif %} +
    +
    + {% if user_bio %} +

    {{user_bio}}

    + {% endif %} +

    Energy Points: {{energy_points}}

    +

    Rank: {{rank}}

    + {% if user_location %} +

    {{user_location}}

    + {% endif %} + {% if user_interest %} +

    {{user_interest}}

    + {% endif %} +
    + +
    \ No newline at end of file diff --git a/frappe/public/js/frappe/ui/toolbar/navbar.html b/frappe/public/js/frappe/ui/toolbar/navbar.html index 42d854280f..035d2ff679 100644 --- a/frappe/public/js/frappe/ui/toolbar/navbar.html +++ b/frappe/public/js/frappe/ui/toolbar/navbar.html @@ -24,7 +24,7 @@ {%= __("Settings") %}