Merge pull request #32040 from sokumon/eps-removal

refactor: separate EPS into an app
This commit is contained in:
Ankush Menat 2025-04-23 17:40:16 +05:30 committed by GitHub
commit 5d380e6f93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 30 additions and 3522 deletions

View file

@ -21,10 +21,6 @@ from frappe.permissions import has_permission
from frappe.query_builder import DocType
from frappe.query_builder.functions import Count
from frappe.query_builder.terms import ParameterizedValueWrapper, SubQuery
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
from frappe.social.doctype.energy_point_settings.energy_point_settings import (
is_energy_point_enabled,
)
from frappe.utils import add_user_info, cstr, get_system_timezone
from frappe.utils.change_log import get_versions
from frappe.utils.frappecloud import on_frappecloud
@ -99,10 +95,7 @@ def get_bootinfo():
bootinfo.lang_dict = get_lang_dict()
bootinfo.success_action = get_success_action()
bootinfo.update(get_email_accounts(user=frappe.session.user))
bootinfo.energy_points_enabled = is_energy_point_enabled()
bootinfo.website_tracking_enabled = is_tracking_enabled()
bootinfo.sms_gateway_enabled = bool(frappe.db.get_single_value("SMS Settings", "sms_gateway_url"))
bootinfo.points = get_energy_points(frappe.session.user)
bootinfo.frequently_visited_links = frequently_visited_links()
bootinfo.link_preview_doctypes = get_link_preview_doctypes()
bootinfo.additional_filters_config = get_additional_filters_from_hooks()

View file

@ -6,7 +6,6 @@ import frappe
common_default_keys = ["__default", "__global"]
doctypes_for_mapping = {
"Energy Point Rule",
"Assignment Rule",
"Milestone Tracker",
"Document Naming Rule",

View file

@ -773,11 +773,6 @@
"group": "Rules",
"link_doctype": "Assignment Rule",
"link_fieldname": "document_type"
},
{
"group": "Rules",
"link_doctype": "Energy Point Rule",
"link_fieldname": "reference_doctype"
}
],
"modified": "2025-03-27 18:16:53.286909",

View file

@ -84,7 +84,7 @@ class TestScheduledJobType(IntegrationTestCase):
def test_weekly_job(self):
job = frappe.get_doc(
"Scheduled Job Type",
dict(method="frappe.social.doctype.energy_point_log.energy_point_log.send_weekly_summary"),
dict(method="frappe.desk.form.document_follow.send_weekly_updates"),
)
job.db_set("last_execution", "2019-01-01 00:00:00")
self.assertTrue(job.is_event_due(get_datetime("2019-01-06 00:10:01"))) # +10 min because of jitter

View file

@ -853,11 +853,6 @@
"link_doctype": "Activity Log",
"link_fieldname": "user"
},
{
"group": "Logs",
"link_doctype": "Energy Point Log",
"link_fieldname": "user"
},
{
"group": "Logs",
"link_doctype": "Route History",

View file

@ -573,9 +573,6 @@ class User(Document):
frappe.db.delete("OAuth Authorization Code", {"user": self.name})
frappe.db.delete("Token Cache", {"user": self.name})
# Delete EPS data
frappe.db.delete("Energy Point Log", {"user": self.name})
# Remove user link from Workflow Action
frappe.db.set_value("Workflow Action", {"user": self.name}, "user", None)

View file

@ -29,7 +29,7 @@ class NotificationLog(Document):
link: DF.Data | None
read: DF.Check
subject: DF.Text | None
type: DF.Literal["", "Mention", "Energy Point", "Assignment", "Share", "Alert"]
type: DF.Literal["", "Mention", "Assignment", "Share", "Alert"]
# end: auto-generated types
def after_insert(self):
@ -103,11 +103,7 @@ def make_notification_logs(doc, users):
notification = frappe.new_doc("Notification Log")
notification.update(doc)
notification.for_user = user
if (
notification.for_user != notification.from_user
or doc.type == "Energy Point"
or doc.type == "Alert"
):
if notification.for_user != notification.from_user or doc.type == "Alert":
notification.insert(ignore_permissions=True)
@ -119,9 +115,6 @@ def _get_user_ids(user_emails):
def send_notification_email(doc: NotificationLog):
if doc.type == "Energy Point" and doc.email_content is None:
return
from frappe.utils import get_url_to_form, strip_html
user = frappe.db.get_value("User", doc.for_user, fieldname=["email", "language"], as_dict=True)
@ -158,7 +151,6 @@ def get_email_header(doc, language: str | None = None):
"Mention": _("New Mention on {0}", lang=language).format(docname),
"Assignment": _("Assignment Update on {0}", lang=language).format(docname),
"Share": _("New Document Shared {0}", lang=language).format(docname),
"Energy Point": _("Energy Point Update on {0}", lang=language).format(docname),
}
return header_map[doc.type or "Default"]

View file

@ -13,13 +13,10 @@
"enable_email_mention",
"enable_email_assignment",
"enable_email_threads_on_assigned_document",
"enable_email_energy_point",
"enable_email_share",
"enable_email_event_reminders",
"user",
"seen",
"system_notifications_section",
"energy_points_system_notifications"
"seen"
],
"fields": [
{
@ -59,13 +56,6 @@
"fieldtype": "Check",
"label": "Assignments"
},
{
"default": "1",
"depends_on": "enable_email_notifications",
"fieldname": "enable_email_energy_point",
"fieldtype": "Check",
"label": "Energy Points"
},
{
"default": "1",
"depends_on": "enable_email_notifications",
@ -89,17 +79,6 @@
"hidden": 1,
"label": "Seen"
},
{
"fieldname": "system_notifications_section",
"fieldtype": "Section Break",
"label": "System Notifications"
},
{
"default": "1",
"fieldname": "energy_points_system_notifications",
"fieldtype": "Check",
"label": "Energy Points"
},
{
"default": "1",
"depends_on": "enable_email_notifications",
@ -119,7 +98,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-03-23 16:03:31.952985",
"modified": "2025-04-16 17:15:25.641232",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Settings",
@ -139,8 +118,9 @@
}
],
"read_only": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View file

@ -18,14 +18,12 @@ class NotificationSettings(Document):
from frappe.types import DF
enable_email_assignment: DF.Check
enable_email_energy_point: DF.Check
enable_email_event_reminders: DF.Check
enable_email_mention: DF.Check
enable_email_notifications: DF.Check
enable_email_share: DF.Check
enable_email_threads_on_assigned_document: DF.Check
enabled: DF.Check
energy_points_system_notifications: DF.Check
seen: DF.Check
subscribed_documents: DF.TableMultiSelect[NotificationSubscribedDocument]
user: DF.Link | None

View file

@ -123,7 +123,6 @@ def get_docinfo(doc=None, doctype=None, name=None):
"permissions": get_doc_permissions(doc),
"shared": get_docshares(doc),
"views": get_view_logs(doc),
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
"additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name),
"milestones": get_milestones(doc.doctype, doc.name),
"is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user),
@ -246,19 +245,6 @@ def get_comments(doctype: str, name: str, comment_type: str | list[str] = "Comme
return comments
def get_point_logs(doctype, docname):
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
if not is_energy_point_enabled():
return []
return frappe.get_all(
"Energy Point Log",
filters={"reference_doctype": doctype, "reference_name": docname, "type": ["!=", "Review"]},
fields=["*"],
)
def _get_communications(doctype, name, start=0, limit=20):
communications = get_communication_data(doctype, name, start, limit)
for c in communications:

View file

@ -1,50 +0,0 @@
import frappe
from frappe.utils import get_fullname
def get_leaderboards():
return {
"User": {
"fields": ["points"],
"method": "frappe.desk.leaderboard.get_energy_point_leaderboard",
"company_disabled": 1,
"icon": "users",
}
}
@frappe.whitelist()
def get_energy_point_leaderboard(date_range, company=None, field=None, limit=None):
users = frappe.get_list(
"User",
filters={
"name": ["not in", ["Administrator", "Guest"]],
"enabled": 1,
"user_type": ["!=", "Website User"],
},
pluck="name",
)
filters = [["type", "!=", "Review"], ["user", "in", users]]
if date_range:
date_range = frappe.parse_json(date_range)
filters.append(["creation", "between", [date_range[0], date_range[1]]])
energy_point_users = frappe.get_all(
"Energy Point Log",
fields=["user as name", "sum(points) as value"],
filters=filters,
group_by="user",
order_by="value desc",
)
energy_point_users_list = list(map(lambda x: x["name"], energy_point_users))
for user in users:
if user not in energy_point_users_list:
energy_point_users.append({"name": user, "value": 0})
for user in energy_point_users:
user_id = user["name"]
user["name"] = get_fullname(user["name"])
user["formatted_name"] = f'<a href="/app/user-profile/{user_id}">{get_fullname(user_id)}</a>'
return energy_point_users

View file

@ -1,85 +0,0 @@
.list-filters {
overflow-y: hidden;
padding: 5px
}
.list-filter-item {
min-width: 150px;
float: left;
margin: 5px;
}
.list-item_content {
flex: 1;
padding-right: 15px;
align-items: center;
}
.select-time, .select-doctype, .select-filter, .select-sort {
background: #f0f4f7;
}
.from-date-field .clearfix{
display: none;
}
.from-date-field {
margin-left: 10px;
}
.select-time:focus, .select-doctype:focus, .select-filter:focus, .select-sort:focus {
background: #f0f4f7;
}
.header-btn-base {
border: none;
outline: 0;
vertical-align: middle;
overflow: hidden;
text-decoration: none;
color: inherit;
background-color: inherit;
cursor: pointer;
white-space: nowrap;
}
.header-btn-round {
border-radius: 4px;
}
.item-title-bold {
font-weight: bold;
}
.rank {
max-width: 100px;
}
.leaderboard .result {
border-top: 1px solid var(--border-color);
}
.leaderboard .list-item {
padding-left: 45px;
}
.leaderboard .list-item_content {
padding-right: 60px;
}
.leaderboard-sidebar {
padding-left: 0;
position: fixed;
}
.leaderboard-list {
padding: var(-padding-sm) 0;
min-height: 70vh;
}
.leaderboard-empty-state {
align-items: center;
height: 70vh;
justify-content: center;
display: flex;
}

View file

@ -1,409 +0,0 @@
frappe.pages["leaderboard"].on_page_load = (wrapper) => {
frappe.leaderboard = new Leaderboard(wrapper);
$(wrapper).bind("show", () => {
// Get which leaderboard to show
let doctype = frappe.get_route()[1];
frappe.leaderboard.show_leaderboard(doctype);
});
};
class Leaderboard {
constructor(parent) {
frappe.ui.make_app_page({
parent: parent,
title: __("Leaderboard"),
single_column: false,
card_layout: true,
});
this.parent = parent;
this.page = this.parent.page;
this.page.sidebar.html(
`<ul class="standard-sidebar leaderboard-sidebar overlay-sidebar"></ul>`
);
this.$sidebar_list = this.page.sidebar.find("ul");
this.get_leaderboard_config();
}
get_leaderboard_config() {
this.doctypes = [];
this.filters = {};
this.leaderboard_limit = 20;
frappe
.xcall("frappe.desk.page.leaderboard.leaderboard.get_leaderboard_config")
.then((config) => {
this.leaderboard_config = config;
for (let doctype in this.leaderboard_config) {
this.doctypes.push(doctype);
this.filters[doctype] = this.leaderboard_config[doctype].fields.map(
(field) => {
if (typeof field === "object") {
return field.label || field.fieldname;
}
return field;
}
);
}
// For translation. Do not remove this
// __("This Week"), __("This Month"), __("This Quarter"), __("This Year"),
// __("Last Week"), __("Last Month"), __("Last Quarter"), __("Last Year"),
// __("All Time"), __("Select From Date")
this.timespans = [
"This Week",
"This Month",
"This Quarter",
"This Year",
"Last Week",
"Last Month",
"Last Quarter",
"Last Year",
"All Time",
"Select Date Range",
];
// for saving current selected filters
const _initial_doctype = frappe.get_route()[1] || this.doctypes[0];
const _initial_timespan = this.timespans[0];
const _initial_filter = this.filters[_initial_doctype];
this.options = {
selected_doctype: _initial_doctype,
selected_filter: _initial_filter,
selected_filter_item: _initial_filter[0],
selected_timespan: _initial_timespan,
};
this.message = null;
this.make();
});
}
make() {
this.$container = $(`<div class="leaderboard page-main-content">
<div class="leaderboard-graph"></div>
<div class="leaderboard-list"></div>
</div>`).appendTo(this.page.main);
this.$graph_area = this.$container.find(".leaderboard-graph");
this.doctypes.map((doctype) => {
const icon = this.leaderboard_config[doctype].icon;
this.get_sidebar_item(doctype, icon).appendTo(this.$sidebar_list);
});
this.setup_leaderboard_fields();
this.render_selected_doctype();
this.render_search_box();
// Get which leaderboard to show
let doctype = frappe.get_route()[1];
this.show_leaderboard(doctype);
}
setup_leaderboard_fields() {
this.company_select = this.page.add_field({
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_default("company"),
reqd: 1,
change: (e) => {
this.make_request();
},
});
this.timespan_select = this.page.add_select(
__("Timespan"),
this.timespans.map((d) => {
return { label: __(d), value: d };
})
);
this.create_date_range_field();
this.type_select = this.page.add_select(
__("Field"),
this.options.selected_filter.map((d) => {
return { label: __(frappe.model.unscrub(d)), value: d };
})
);
this.timespan_select.on("change", (e) => {
this.options.selected_timespan = e.currentTarget.value;
if (this.options.selected_timespan === "Select Date Range") {
this.date_range_field.show();
} else {
this.date_range_field.hide();
}
this.make_request();
});
this.type_select.on("change", (e) => {
this.options.selected_filter_item = e.currentTarget.value;
this.make_request();
});
}
create_date_range_field() {
let timespan_field = $(this.parent).find(
`.frappe-control[data-original-title="${__("Timespan")}"]`
);
this.date_range_field = $(`<div class="from-date-field"></div>`)
.insertAfter(timespan_field)
.hide();
let date_field = frappe.ui.form.make_control({
df: {
fieldtype: "DateRange",
fieldname: "selected_date_range",
placeholder: __("Date Range"),
default: [frappe.datetime.month_start(), frappe.datetime.now_date()],
input_class: "input-xs",
reqd: 1,
change: () => {
this.selected_date_range = date_field.get_value();
if (this.selected_date_range) this.make_request();
},
},
parent: $(this.parent).find(".from-date-field"),
render_input: 1,
});
}
render_selected_doctype() {
this.$sidebar_list.on("click", "li", (e) => {
let $li = $(e.currentTarget);
let doctype = $li.find(".doctype-text").attr("doctype-value");
this.company_select.set_value(
frappe.defaults.get_default("company") || this.company_select.get_value()
);
this.options.selected_doctype = doctype;
this.options.selected_filter = this.filters[doctype];
this.options.selected_filter_item = this.filters[doctype][0];
this.type_select.empty().add_options(
this.options.selected_filter.map((d) => {
return { label: __(frappe.model.unscrub(d)), value: d };
})
);
if (this.leaderboard_config[this.options.selected_doctype].company_disabled) {
$(this.parent).find("[data-original-title=Company]").hide();
} else {
$(this.parent).find("[data-original-title=Company]").show();
}
this.$sidebar_list.find("li").removeClass("active selected");
$li.addClass("active selected");
frappe.set_route("leaderboard", this.options.selected_doctype);
this.make_request();
});
}
render_search_box() {
this.$search_box = $(`<div class="leaderboard-search form-group col-md-3">
<input type="text" placeholder=${__(
"Search"
)} data-element="search" class="form-control leaderboard-search-input input-xs">
</div>`);
$(this.parent).find(".page-form").append(this.$search_box);
}
show_leaderboard(doctype) {
if (this.doctypes.length) {
if (this.doctypes.includes(doctype)) {
this.options.selected_doctype = doctype;
this.$sidebar_list
.find(`[doctype-value = "${this.options.selected_doctype}"]`)
.trigger("click");
}
this.$search_box.find(".leaderboard-search-input").val("");
frappe.set_route("leaderboard", this.options.selected_doctype);
}
}
make_request() {
frappe.model.with_doctype(this.options.selected_doctype, () => {
this.get_leaderboard(this.get_leaderboard_data);
});
}
get_leaderboard(notify) {
let company = this.company_select.get_value();
if (!company && !this.leaderboard_config[this.options.selected_doctype].company_disabled) {
notify(this, null);
frappe.show_alert(__("Please select Company"));
return;
}
frappe
.call(this.leaderboard_config[this.options.selected_doctype].method, {
date_range: this.get_date_range(),
company: company,
field: this.options.selected_filter_item,
limit: this.leaderboard_limit,
})
.then((r) => {
let results = r.message || [];
let graph_items = results.slice(0, 10);
this.$graph_area.show().empty();
const custom_options = {
data: {
datasets: [{ values: graph_items.map((d) => d.value) }],
labels: graph_items.map((d) => d.name),
},
format_tooltip_x: (d) => d[this.options.selected_filter_item],
height: 140,
};
frappe.utils.make_chart(".leaderboard-graph", custom_options);
notify(this, r);
});
}
get_leaderboard_data(me, res) {
if (res && res.message.length) {
me.message = null;
me.$container.find(".leaderboard-list").html(me.render_list_view(res.message));
frappe.utils.setup_search($(me.parent), ".list-item-container", ".list-id");
} else {
me.$graph_area.hide();
me.message = __("No Items Found");
me.$container.find(".leaderboard-list").html(me.render_list_view());
}
}
render_list_view(items = []) {
var html = `${this.render_message()}
<div class="result" style="${this.message ? "display: none;" : ""}">
${this.render_result(items)}
</div>`;
return $(html);
}
render_result(items) {
var html = `${this.render_list_header()}
${this.render_list_result(items)}`;
return html;
}
render_list_header() {
const _selected_filter = this.options.selected_filter.map((i) => frappe.model.unscrub(i));
const fields = ["rank", "name", this.options.selected_filter_item];
const filters = fields
.map((filter) => {
const col = __(frappe.model.unscrub(filter));
return `<div class="leaderboard-item list-item_content ellipsis text-muted list-item__content--flex-2
header-btn-base ${filter}
${col && _selected_filter.indexOf(col) !== -1 ? "text-right" : ""}">
<span class="list-col-title ellipsis">
${col}
</span>
</div>`;
})
.join("");
return `<div class="list-headers">
<div class="list-item" data-list-renderer="List">${filters}</div>
</div>`;
}
render_list_result(items) {
let _html = items
.map((item, index) => {
const $value = $(this.get_item_html(item, index + 1));
const $item_container = $(`<div class="list-item-container">`).append($value);
return $item_container[0].outerHTML;
})
.join("");
return `<div class="result-list">
<div class="list-items">
${_html}
</div>
</div>`;
}
render_message() {
const display_class = this.message ? "" : "hide";
return `<div class="leaderboard-empty-state ${display_class}">
<div class="no-result text-center">
<img src="/assets/frappe/images/ui-states/search-empty-state.svg"
alt="Empty State"
class="null-state"
>
<div class="empty-state-text">${this.message}</div>
</div>
</div>`;
}
get_item_html(item, index) {
const fields = this.leaderboard_config[this.options.selected_doctype].fields;
const value = frappe.format(
item.value,
fields.find((field) => {
let fieldname = field.fieldname || field;
return fieldname === this.options.selected_filter_item;
})
);
const link = `/app/${frappe.router.slug(this.options.selected_doctype)}/${item.name}`;
const name_html = item.formatted_name
? `<span class="text-muted ellipsis list-id">${item.formatted_name}</span>`
: `<a class="grey list-id ellipsis" href="${link}"> ${item.name} </a>`;
return `<div class="list-item">
<div class="list-item_content ellipsis list-item__content--flex-2 rank text-center">
<span class="text-muted ellipsis">${index}</span>
</div>
<div class="list-item_content ellipsis list-item__content--flex-2 name">
${name_html}
</div>
<div class="list-item_content ellipsis list-item__content--flex-2 value text-right">
<span class="text-muted ellipsis">${value}</span>
</div>
</div>`;
}
get_sidebar_item(item, icon) {
let icon_html = icon ? frappe.utils.icon(icon, "md") : "";
return $(`<li class="standard-sidebar-item">
<span>${icon_html}</span>
<a class="sidebar-link">
<span class="doctype-text" doctype-value="${item}">${__(item)}</span>
</a>
</li>`);
}
get_date_range() {
let timespan = this.options.selected_timespan.toLowerCase();
let current_date = frappe.datetime.now_date();
let date_range_map = {
"this week": [frappe.datetime.week_start(), frappe.datetime.week_end()],
"this month": [frappe.datetime.month_start(), frappe.datetime.month_end()],
"this quarter": [frappe.datetime.quarter_start(), frappe.datetime.quarter_end()],
"this year": [frappe.datetime.year_start(), frappe.datetime.year_end()],
"last week": [frappe.datetime.add_days(current_date, -7), current_date],
"last month": [frappe.datetime.add_months(current_date, -1), current_date],
"last quarter": [frappe.datetime.add_months(current_date, -3), current_date],
"last year": [frappe.datetime.add_months(current_date, -12), current_date],
"all time": null,
"select date range": this.selected_date_range || [
frappe.datetime.month_start(),
current_date,
],
};
return date_range_map[timespan];
}
}

View file

@ -1,19 +0,0 @@
{
"content": null,
"creation": "2017-06-06 02:54:24.785360",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2019-09-27 17:44:51.909947",
"modified_by": "Administrator",
"module": "Desk",
"name": "leaderboard",
"owner": "Administrator",
"page_name": "leaderboard",
"roles": [],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0,
"title": "Leaderboard"
}

View file

@ -1,13 +0,0 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
@frappe.whitelist()
def get_leaderboard_config():
leaderboard_config = frappe._dict()
leaderboard_hooks = frappe.get_hooks("leaderboards")
for hook in leaderboard_hooks:
leaderboard_config.update(frappe.get_attr(hook)())
return leaderboard_config

View file

@ -1,30 +0,0 @@
.recent-activity .new-timeline {
padding-top: 0;
}
.recent-activity .new-timeline:before {
top: 25px;
}
.recent-activity-title {
font-weight: 700;
font-size: var(--text-xl);
color: var(--text-color);
}
.recent-activity .recent-activity-footer {
margin-left: calc(var(--timeline-left-padding) + var(--timeline-item-left-margin));
max-width: var(--timeline-content-max-width);
}
.recent-activity .show-more-activity-btn {
display: block;
margin: auto;
width: max-content;
margin-top: 35px;
font-size: var(--text-md);
}
.recent-activity {
padding-bottom: 60px;
}

View file

@ -1,44 +0,0 @@
<div class="row user-profile">
<div class="col-md-12">
<div class="performance-graphs">
<div class="frappe-card chart-column-container heatmap-container hidden-xs hidden-sm">
<div class="title-area">
<h4 class="card-title">{%=__("Overview") %}</h4>
<div class="card-options heatmap-options"></div>
</div>
<div class="chart-wrapper performance-heatmap">
<div class="null-state">
<span>{%=__("No Data to Show") %}</span>
</div>
</div>
</div>
<div class="frappe-card chart-column-container percentage-chart-container">
<div class="title-area">
<h4 class="card-title">{%=__("Type Distribution") %}</h4>
<div class="card-options percentage-chart-options"></div>
</div>
<div class="chart-wrapper performance-percentage-chart">
<div class="null-state">
<span>{%=__("No Data to Show") %}</span>
</div>
</div>
</div>
<div class="frappe-card chart-column-container line-chart-container">
<div class="title-area">
<h4 class="card-title">{%=__("Energy Points") %}</h4>
<div class="card-options line-chart-options"></div>
</div>
<div class="chart-wrapper performance-line-chart">
<div class="null-state">
<span>{%=__("No Data to Show") %}</span>
</div>
</div>
</div>
</div>
<div class="recent-activity">
<div class="recent-activity-title">{%=__("Recent Activity") %}</div>
<div class="recent-activity-list"></div>
<div class="recent-activity-footer"></div>
</div>
</div>
</div>

View file

@ -1,6 +0,0 @@
frappe.pages["user-profile"].on_page_load = function (wrapper) {
frappe.require("user_profile_controller.bundle.js", () => {
let user_profile = new frappe.ui.UserProfile(wrapper);
user_profile.show();
});
};

View file

@ -1,23 +0,0 @@
{
"content": null,
"creation": "2019-07-22 12:23:38.425877",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2020-03-02 15:17:13.041650",
"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,
"title": "User Profile"
}

View file

@ -1,112 +0,0 @@
from datetime import datetime
import frappe
from frappe.query_builder import Interval, Order
from frappe.query_builder.functions import Date, Sum, UnixTimestamp
from frappe.utils import getdate
@frappe.whitelist()
def get_energy_points_heatmap_data(user, date):
try:
date = getdate(date)
except Exception:
date = getdate()
eps_log = frappe.qb.DocType("Energy Point Log")
return dict(
frappe.qb.from_(eps_log)
.select(UnixTimestamp(Date(eps_log.creation)), Sum(eps_log.points))
.where(eps_log.user == user)
.where(eps_log["type"] != "Review")
.where(Date(eps_log.creation) > Date(date) - Interval(years=1))
.where(Date(eps_log.creation) < Date(date) + Interval(years=1))
.groupby(Date(eps_log.creation))
.orderby(Date(eps_log.creation), order=Order.asc)
.run()
)
@frappe.whitelist()
def get_energy_points_percentage_chart_data(user, field):
result = frappe.get_all(
"Energy Point Log",
filters={"user": user, "type": ["!=", "Review"]},
group_by=field,
order_by=field,
fields=[field, "ABS(sum(points)) as points"],
as_list=True,
)
return {
"labels": [r[0] for r in result if r[0] is not None],
"datasets": [{"values": [r[1] for r in result]}],
}
@frappe.whitelist()
def get_user_rank(user):
month_start = datetime.today().replace(day=1)
monthly_rank = frappe.get_all(
"Energy Point Log",
group_by="`tabEnergy Point Log`.`user`",
filters={"creation": [">", month_start], "type": ["!=", "Review"]},
fields=["user", "sum(points)"],
order_by="sum(points) desc",
as_list=True,
)
all_time_rank = frappe.get_all(
"Energy Point Log",
group_by="`tabEnergy Point Log`.`user`",
filters={"type": ["!=", "Review"]},
fields=["user", "sum(points)"],
order_by="sum(points) desc",
as_list=True,
)
return {
"monthly_rank": [i + 1 for i, r in enumerate(monthly_rank) if r[0] == user],
"all_time_rank": [i + 1 for i, r in enumerate(all_time_rank) if r[0] == user],
}
@frappe.whitelist()
def update_profile_info(profile_info):
profile_info = frappe.parse_json(profile_info)
keys = ["location", "interest", "user_image", "bio"]
for key in keys:
if key not in profile_info:
profile_info[key] = 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",
"revert_of",
],
start=start,
limit=limit,
order_by="creation desc",
)

View file

@ -1,494 +0,0 @@
import BaseTimeline from "../../../public/js/frappe/form/footer/base_timeline";
frappe.provide("frappe.energy_points");
class UserProfile {
constructor(wrapper) {
this.wrapper = $(wrapper);
this.page = frappe.ui.make_app_page({
parent: wrapper,
});
this.sidebar = this.wrapper.find(".layout-side-section");
this.main_section = this.wrapper.find(".layout-main-section");
this.wrapper.bind("show", () => {
this.show();
});
}
show() {
let route = frappe.get_route();
this.user_id = route[1] || frappe.session.user;
frappe.dom.freeze(__("Loading user profile") + "...");
frappe.db.exists("User", this.user_id).then((exists) => {
frappe.dom.unfreeze();
if (exists) {
this.make_user_profile();
} else {
frappe.msgprint(__("User does not exist"));
}
});
}
make_user_profile() {
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.review_points = 0;
this.rank = 0;
this.month_rank = 0;
this.render_user_details();
this.render_points_and_rank();
this.render_heatmap();
this.render_line_chart();
this.render_percentage_chart("type", "Type Distribution");
this.create_percentage_chart_filters();
this.setup_user_activity_timeline();
}
setup_user_search() {
this.$user_search_button = this.page.set_secondary_action(
__("Change User"),
() => this.show_user_search_dialog(),
{ icon: "change", size: "sm" }
);
}
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();
frappe.set_route("user-profile", user);
},
});
dialog.show();
}
render_heatmap() {
this.heatmap = new frappe.Chart(".performance-heatmap", {
type: "heatmap",
countLabel: __("Energy Points"),
data: {},
discreteDomains: 1,
radius: 3,
height: 150,
});
this.update_heatmap_data();
this.create_heatmap_chart_filters();
}
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_line_chart() {
this.line_chart_filters = [
["Energy Point Log", "user", "=", this.user_id, false],
["Energy Point Log", "type", "!=", "Review", false],
];
this.line_chart_config = {
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", {
type: "line",
height: 200,
data: {
labels: [],
datasets: [{}],
},
colors: ["purple"],
axisOptions: {
xIsSeries: 1,
},
});
this.update_line_chart_data();
this.create_line_chart_filters();
}
update_line_chart_data() {
this.line_chart_config.filters_json = JSON.stringify(this.line_chart_filters);
frappe
.xcall("frappe.desk.doctype.dashboard_chart.dashboard_chart.get", {
chart: this.line_chart_config,
no_cache: 1,
})
.then((chart) => {
this.line_chart.update(chart);
});
}
render_percentage_chart(field, title) {
frappe
.xcall(
"frappe.desk.page.user_profile.user_profile.get_energy_points_percentage_chart_data",
{
user: this.user_id,
field: field,
}
)
.then((chart) => {
if (chart.labels.length) {
this.percentage_chart = new frappe.Chart(".performance-percentage-chart", {
type: "percentage",
data: {
labels: chart.labels,
datasets: chart.datasets,
},
truncateLegends: 1,
barOptions: {
height: 11,
depth: 1,
},
height: 200,
maxSlices: 8,
colors: [
"purple",
"blue",
"cyan",
"teal",
"pink",
"red",
"orange",
"yellow",
],
});
} else {
this.wrapper.find(".percentage-chart-container").hide();
}
});
}
create_line_chart_filters() {
let filters = [
{
label: "All",
options: ["All", "Auto", "Criticism", "Appreciation", "Revert"],
action: (selected_item) => {
if (selected_item === "All") {
this.line_chart_filters = [
["Energy Point Log", "user", "=", this.user_id, false],
["Energy Point Log", "type", "!=", "Review", false],
];
} else {
this.line_chart_filters[1] = [
"Energy Point Log",
"type",
"=",
selected_item,
false,
];
}
this.update_line_chart_data();
},
},
{
label: "Last Month",
options: ["Last Week", "Last Month", "Last Quarter", "Last Year"],
action: (selected_item) => {
this.line_chart_config.timespan = selected_item;
this.update_line_chart_data();
},
},
{
label: "Daily",
options: ["Daily", "Weekly", "Monthly"],
action: (selected_item) => {
this.line_chart_config.time_interval = selected_item;
this.update_line_chart_data();
},
},
];
frappe.dashboard_utils.render_chart_filters(
filters,
"chart-filter",
".line-chart-options",
1
);
}
create_percentage_chart_filters() {
let filters = [
{
label: "Type",
options: ["Type", "Reference Doctype", "Rule"],
fieldnames: ["type", "reference_doctype", "rule"],
action: (selected_item, fieldname) => {
let title = selected_item + " Distribution";
this.render_percentage_chart(fieldname, title);
},
},
];
frappe.dashboard_utils.render_chart_filters(
filters,
"chart-filter",
".percentage-chart-options"
);
}
create_heatmap_chart_filters() {
let filters = [
{
label: frappe.dashboard_utils.get_year(frappe.datetime.now_date()),
options: frappe.dashboard_utils.get_years_since_creation(
frappe.boot.user.creation
),
action: (selected_item) => {
this.update_heatmap_data(frappe.datetime.obj_to_str(selected_item));
},
},
];
frappe.dashboard_utils.render_chart_filters(filters, "chart-filter", ".heatmap-options");
}
edit_profile() {
let 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.sidebar.empty().append(
frappe.render_template("user_profile_sidebar", {
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,
})
);
this.setup_user_profile_links();
}
setup_user_profile_links() {
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_rank() {
return frappe
.xcall("frappe.desk.page.user_profile.user_profile.get_user_rank", {
user: this.user_id,
})
.then((r) => {
if (r.monthly_rank.length) this.month_rank = r.monthly_rank[0];
if (r.all_time_rank.length) this.rank = r.all_time_rank[0];
});
}
get_user_points() {
return frappe
.xcall(
"frappe.social.doctype.energy_point_log.energy_point_log.get_user_energy_and_review_points",
{
user: this.user_id,
}
)
.then((r) => {
if (r[this.user_id]) {
this.energy_points = r[this.user_id].energy_points;
this.review_points = r[this.user_id].review_points;
}
});
}
render_points_and_rank() {
let $profile_details = this.wrapper.find(".user-stats");
let $profile_details_wrapper = this.wrapper.find(".user-stats-detail");
const _get_stat_dom = (value, label, icon) => {
return `<div class="user-stats-item mt-4">
${frappe.utils.icon(icon, "lg", "no-stroke")}
<div>
<div class="stat-value">${value}</div>
<div class="stat-label">${label}</div>
</div>
</div>`;
};
this.get_user_rank().then(() => {
this.get_user_points().then(() => {
let html = $(`
${_get_stat_dom(this.energy_points, __("Energy Points"), "color-energy-points")}
${_get_stat_dom(this.review_points, __("Review Points"), "color-review-points")}
${_get_stat_dom(this.rank, __("Rank"), "color-rank")}
${_get_stat_dom(this.month_rank, __("Monthly Rank"), "color-monthly-rank")}
`);
$profile_details.append(html);
$profile_details_wrapper.removeClass("hide");
});
});
}
go_to_user_settings() {
frappe.set_route("Form", "User", this.user_id);
}
setup_user_activity_timeline() {
this.user_activity_timeline = new UserProfileTimeline({
parent: this.wrapper.find(".recent-activity-list"),
footer: this.wrapper.find(".recent-activity-footer"),
user: this.user_id,
});
this.user_activity_timeline.refresh();
}
}
class UserProfileTimeline extends BaseTimeline {
make() {
super.make();
this.activity_start = 0;
this.activity_limit = 20;
this.setup_show_more_activity();
}
prepare_timeline_contents() {
return this.get_user_activity_data().then((activities) => {
if (!activities.length) {
this.show_more_button.hide();
this.timeline_wrapper.html(`<div>${__("No activities to show")}</div>`);
return;
}
this.show_more_button.toggle(activities.length === this.activity_limit);
this.timeline_items = activities.map((activity) =>
this.get_activity_timeline_item(activity)
);
});
}
get_user_activity_data() {
return frappe.xcall("frappe.desk.page.user_profile.user_profile.get_energy_points_list", {
start: this.activity_start,
limit: this.activity_limit,
user: this.user,
});
}
get_activity_timeline_item(data) {
let icon =
data.type == "Appreciation" ? "clap" : data.type == "Criticism" ? "criticize" : null;
return {
icon: icon,
creation: data.creation,
is_card: true,
content: frappe.energy_points.format_history_log(data),
};
}
setup_show_more_activity() {
this.show_more_button = $(
`<a class="show-more-activity-btn">${__("Show More Activity")}</a>`
);
this.show_more_button.hide();
this.footer.append(this.show_more_button);
this.show_more_button.on("click", () => this.show_more_activity());
}
show_more_activity() {
this.activity_start += this.activity_limit;
this.get_user_activity_data().then((activities) => {
if (!activities.length || activities.length < this.activity_limit) {
this.show_more_button.hide();
}
let timeline_items = activities.map((activity) =>
this.get_activity_timeline_item(activity)
);
timeline_items.map((item) => this.add_timeline_item(item, true));
});
}
}
frappe.provide("frappe.ui");
frappe.ui.UserProfile = UserProfile;

View file

@ -1,60 +0,0 @@
<div class="user-profile-sidebar">
<div class="user-image-container hidden-xs hidden-sm">
<div class="sidebar-image-wrapper">
{% if user_image %}
<img class="sidebar-image" src="{{ user_image }}" />
{% else %}
<div class="sidebar-standard-image hide">
<div class="standard-image"></div>
</div>
{% endif %}
</div>
</div>
<div class="profile-details">
{% if user_bio %}
<div class="detail-item">
<p class="user-bio">{{user_bio}}</p>
</div>
{% endif %}
{% if user_location %}
<div class="detail-item">
<h5>{%=__("Intro") %}</h5>
<p class="user-location text-muted mt-3">
<span class="interest-icon">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.4827 6.57143C12.4827 9.39265 7.91127 14 7.91127 14C7.91127 14 3.33984 9.39265 3.33984 6.57143C3.33984 3.67837 5.70197 2 7.91127 2C10.1206 2 12.4827 3.67837 12.4827 6.57143Z" stroke="#192734" stroke-miterlimit="10" stroke-linecap="square" stroke-linejoin="round"/>
<path d="M7.91354 7.87763C8.6349 7.87763 9.21967 7.29285 9.21967 6.5715C9.21967 5.85015 8.6349 5.26538 7.91354 5.26538C7.19219 5.26538 6.60742 5.85015 6.60742 6.5715C6.60742 7.29285 7.19219 7.87763 7.91354 7.87763Z" stroke="#192734" stroke-miterlimit="10" stroke-linecap="square"/>
</svg>
</span>
{{user_location}}
</p>
</div>
{% endif %}
{% if user_interest %}
<div class="detail-item">
<h5>{%=__("Interests") %}</h5>
<p class="user-interests text-muted mt-3">
{{user_interest}}
</p>
</div>
{% endif %}
<div class="detail-item user-stats-detail hide">
<h5>{%=__("Details") %}</h5>
<div class="user-stats mt-3">
</div>
</div>
<div class="profile-links detail-item">
<p><a class="edit-profile-link">{%=__("Edit Profile") %}</a></p>
<p><a class="user-settings-link">{%=__("User Settings") %}</a></p>
<p>
<a class="leaderboard-link" href="/app/leaderboard/User"
>{%=__("Leaderboard") %}</a
>
</p>
</div>
</div>
</div>

View file

@ -81,8 +81,6 @@ email_append_to = ["Event", "ToDo", "Communication"]
calendars = ["Event"]
leaderboards = "frappe.desk.leaderboard.get_leaderboards"
# login
on_session_creation = [
@ -177,7 +175,6 @@ doc_events = {
"frappe.core.doctype.file.utils.attach_files_to_document",
],
"on_change": [
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points",
"frappe.automation.doctype.milestone_tracker.milestone_tracker.evaluate_milestone",
],
"after_delete": ["frappe.core.doctype.permission_log.permission_log.make_perm_log"],
@ -248,7 +245,6 @@ scheduler_events = {
"frappe.email.doctype.notification.notification.trigger_daily_alerts",
"frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.remove_unverified_record",
"frappe.desk.form.document_follow.send_daily_updates",
"frappe.social.doctype.energy_point_settings.energy_point_settings.allocate_review_points",
"frappe.integrations.doctype.google_contacts.google_contacts.sync",
"frappe.automation.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry",
],
@ -263,13 +259,11 @@ scheduler_events = {
"frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_weekly",
"frappe.desk.form.document_follow.send_weekly_updates",
"frappe.utils.change_log.check_for_update",
"frappe.social.doctype.energy_point_log.energy_point_log.send_weekly_summary",
"frappe.integrations.doctype.google_drive.google_drive.weekly_backup",
"frappe.desk.doctype.changelog_feed.changelog_feed.fetch_changelog_feed",
],
"monthly": [
"frappe.email.doctype.auto_email_report.auto_email_report.send_monthly",
"frappe.social.doctype.energy_point_log.energy_point_log.send_monthly_summary",
],
"monthly_long": [
"frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_monthly"

View file

@ -199,8 +199,6 @@ def rename_doc(
rename_versions(doctype, old, new)
rename_eps_records(doctype, old, new)
# call after_rename
new_doc = frappe.get_doc(doctype, new)
@ -329,14 +327,6 @@ def rename_versions(doctype: str, old: str, new: str) -> None:
).run()
def rename_eps_records(doctype: str, old: str, new: str) -> None:
EPL = frappe.qb.DocType("Energy Point Log")
frappe.qb.update(EPL).set(EPL.reference_name, new).where(
(EPL.reference_doctype == doctype) & (EPL.reference_name == old)
).run()
def rename_parent_and_child(doctype: str, old: str, new: str, meta: "Meta") -> None:
frappe.qb.update(doctype).set("name", new).where(Field("name") == old).run()

View file

@ -163,7 +163,7 @@ def remove_orphan_doctypes():
continue
try:
get_controller(doctype=doctype)
except ImportError:
except (ImportError, frappe.DoesNotExistError):
orphan_doctypes.append(doctype)
except Exception:
continue

View file

@ -20,7 +20,6 @@ ignore_doctypes = {
"Access Log",
"View Log",
"Activity Log",
"Energy Point Log",
"Notification Log",
"Email Queue",
"DocShare",

View file

@ -8,5 +8,4 @@ Desk
Integrations
Printing
Contacts
Social
Automation

View file

@ -245,4 +245,4 @@ frappe.desk.doctype.workspace.patches.update_app
frappe.patches.v16_0.move_role_desk_settings_to_user
frappe.printing.doctype.print_format.patches.sets_wkhtmltopdf_as_default_for_pdf_generator_field
frappe.patches.v14_0.fix_user_settings_collation
execute:frappe.core.doctype.system_settings.system_settings.sync_system_settings
execute:frappe.core.doctype.system_settings.system_settings.sync_system_settings

View file

@ -101,7 +101,6 @@ import "./frappe/ui/workspace_sidebar_loading_skeleton.html";
import "./frappe/desk.js";
import "./frappe/query_string.js";
import "./frappe/utils/energy_point_utils.js";
import "./frappe/utils/dashboard_utils.js";
import "./frappe/ui/chart.js";
import "./frappe/ui/datatable.js";

View file

@ -41,7 +41,6 @@ frappe.Application = class Application {
this.set_favicon();
this.set_fullwidth_if_enabled();
this.add_browser_class();
this.setup_energy_point_listeners();
this.setup_copy_doc_listener();
this.setup_broadcast_listeners();
@ -499,12 +498,6 @@ frappe.Application = class Application {
}
}
setup_energy_point_listeners() {
frappe.realtime.on("energy_point_alert", (message) => {
frappe.show_alert(message);
});
}
setup_copy_doc_listener() {
$("body").on("paste", (e) => {
try {

View file

@ -148,4 +148,5 @@ class BaseTimeline {
}
}
frappe.ui.BaseTimeline = BaseTimeline;
export default BaseTimeline;

View file

@ -161,7 +161,6 @@ class FormTimeline extends BaseTimeline {
this.timeline_items.push(...this.get_comment_timeline_contents());
if (!this.only_communication) {
this.timeline_items.push(...this.get_view_timeline_contents());
this.timeline_items.push(...this.get_energy_point_timeline_contents());
this.timeline_items.push(...this.get_version_timeline_contents());
this.timeline_items.push(...this.get_share_timeline_contents());
this.timeline_items.push(...this.get_workflow_timeline_contents());
@ -494,36 +493,30 @@ class FormTimeline extends BaseTimeline {
get_custom_timeline_contents() {
let custom_timeline_contents = [];
(this.doc_info.additional_timeline_content || []).forEach((custom_item) => {
custom_timeline_contents.push({
icon: custom_item.icon,
icon_size: "sm",
is_card: custom_item.is_card,
creation: custom_item.creation,
content:
custom_item.content ||
frappe.render_template(custom_item.template, custom_item.template_data),
});
if (custom_item.timeline_badge) {
custom_timeline_contents.push({
timeline_badge: custom_item.timeline_badge,
creation: custom_item.creation,
content: frappe.utils.eval(custom_item.method, {
custom_item: custom_item,
}),
});
} else {
custom_timeline_contents.push({
icon: custom_item.icon,
timeline_badge: custom_item.timeline_badge,
icon_size: "sm",
is_card: custom_item.is_card,
creation: custom_item.creation,
content:
custom_item.content ||
frappe.render_template(custom_item.template, custom_item.template_data),
});
}
});
return custom_timeline_contents;
}
get_energy_point_timeline_contents() {
let energy_point_timeline_contents = [];
(this.doc_info.energy_point_logs || []).forEach((log) => {
let timeline_badge = `
<div class="timeline-badge ${log.points > 0 ? "appreciation" : "criticism"} bold">
${log.points}
</div>`;
energy_point_timeline_contents.push({
timeline_badge: timeline_badge,
creation: log.creation,
content: frappe.energy_points.format_form_log(log),
});
});
return energy_point_timeline_contents;
}
setup_reply(communication_box, communication_doc) {
let actions = communication_box.find(".custom-actions");
let reply = $(`<a class="action-btn reply">${frappe.utils.icon("reply", "md")}</a>`).click(

View file

@ -1,7 +1,6 @@
import "./assign_to";
import "./attachments";
import "./share";
import "./review";
import "./document_follow";
import "./user_image";
import "./form_sidebar_users";
@ -28,7 +27,6 @@ frappe.ui.form.Sidebar = class {
this.image_wrapper = this.image_section.find(".sidebar-image-wrapper");
this.make_assignments();
this.make_attachments();
this.make_review();
this.make_shared();
this.make_tags();
@ -214,18 +212,6 @@ frappe.ui.form.Sidebar = class {
refresh_image() {}
make_review() {
const review_wrapper = this.sidebar.find(".form-reviews");
if (frappe.boot.energy_points_enabled && !this.frm.is_new()) {
this.frm.reviews = new frappe.ui.form.Review({
parent: review_wrapper,
frm: this.frm,
});
} else {
review_wrapper.remove();
}
}
reload_docinfo(callback) {
frappe.call({
method: "frappe.desk.form.load.get_docinfo",

View file

@ -1,198 +0,0 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
frappe.ui.form.Review = class Review {
constructor({ parent, frm }) {
this.parent = parent;
this.frm = frm;
this.points = frappe.boot.points;
this.reviews = this.parent.find(".reviews");
this.setup_add_review_button();
this.update_reviewers();
}
update_points() {
return frappe
.xcall("frappe.social.doctype.energy_point_log.energy_point_log.get_energy_points", {
user: frappe.session.user,
})
.then((data) => {
frappe.boot.points = data;
this.points = data;
});
}
setup_add_review_button() {
const review_button = this.reviews.find(".add-review-btn");
if (!this.points.review_points) {
review_button.click(false);
review_button.popover({
trigger: "hover",
content: () => {
return `<div class="text-medium">
${__("You do not have enough review points")}
</div>`;
},
html: true,
});
} else {
review_button.click(() => this.show_review_dialog());
}
}
show_review_dialog() {
const user_options = this.frm.get_involved_users();
const review_dialog = new frappe.ui.Dialog({
title: __("Add Review"),
fields: [
{
fieldname: "to_user",
fieldtype: "Autocomplete",
label: __("To User"),
reqd: 1,
options: user_options,
ignore_validation: 1,
description: __("Only users involved in the document are listed"),
},
{
fieldname: "review_type",
fieldtype: "Select",
label: __("Action"),
options: [
{
label: __("Appreciate"),
value: "Appreciation",
},
{
label: __("Criticize"),
value: "Criticism",
},
],
default: "Appreciation",
},
{
fieldname: "points",
fieldtype: "Int",
label: __("Points"),
reqd: 1,
description: __("Currently you have {0} review points", [
this.points.review_points,
]),
},
{
fieldtype: "Small Text",
fieldname: "reason",
reqd: 1,
label: __("Reason"),
},
],
primary_action: (values) => {
review_dialog.disable_primary_action();
if (values.points > this.points.review_points) {
return frappe.msgprint(__("You do not have enough points"));
}
frappe
.xcall("frappe.social.doctype.energy_point_log.energy_point_log.review", {
doc: {
doctype: this.frm.doc.doctype,
name: this.frm.doc.name,
},
to_user: values.to_user,
points: values.points,
review_type: values.review_type,
reason: values.reason,
})
.then((review) => {
review_dialog.hide();
review_dialog.clear();
this.frm.get_docinfo().energy_point_logs.unshift(review);
this.frm.timeline.refresh();
this.update_reviewers();
this.update_points();
})
.finally(() => {
review_dialog.enable_primary_action();
});
},
primary_action_label: __("Submit"),
});
review_dialog.show();
}
update_reviewers() {
const review_logs = this.frm
.get_docinfo()
.energy_point_logs.filter((log) => ["Appreciation", "Criticism"].includes(log.type));
this.reviews.find(".review").remove();
review_logs.forEach((log) => {
let review_pill = $(`
<div class="review ${log.points < 0 ? "criticism" : "appreciation"} cursor-pointer">
${frappe.avatar(log.owner)}
<span class="review-points">
${log.points > 0 ? "+" : ""}${log.points}
</span>
</div>
`);
this.reviews.append(review_pill);
this.setup_detail_popover(review_pill, log);
});
}
setup_detail_popover(el, data) {
let subject = "";
let fullname = frappe.user.full_name(data.user);
let timestamp = `<span class="text-muted">${frappe.datetime.comment_when(
data.creation
)}</span>`;
let message_parts = [Math.abs(data.points), fullname, timestamp];
if (data.type === "Appreciation") {
if (data.points == 1) {
subject = __("{0} appreciation point for {1}", message_parts);
} else {
subject = __("{0} appreciation points for {1}", message_parts);
}
} else {
if (data.points == -1) {
subject = __("{0} criticism point for {1}", message_parts);
} else {
subject = __("{0} criticism points for {1}", message_parts);
}
}
el.popover({
animation: true,
trigger: "hover",
delay: 500,
placement: "top",
template: `
<div class="review-popover popover" role="tooltip">
<div class="arrow"></div>
<h3 class="popover-header"></h3>
<div class="popover-body">
</div>
</div>'
`,
content: () => {
return `
<div class="text-medium">
<div class="body">
<div>${data.reason}</div>
</div>
<div class="subject">
${frappe.utils.icon("review")}
${subject}
<p class="mt-1">
<!-- ${frappe.avatar("shivam@example.com", "avatar-xs")} -->
<span>${frappe.user.full_name(data.owner)}</span> - ${timestamp}
</p>
</div>
</div>
`;
},
html: true,
container: "body",
});
return el;
}
};

View file

@ -74,18 +74,6 @@
</span>
</a>
</div>
<div class="sidebar-section form-reviews">
<div class="reviews">
<span class="form-sidebar-items">
<span>
<span class="ellipsis">{%= __("Reviews") %}</span>
</span>
<button class="add-review-btn btn btn-link icon-btn">
<svg class="es-icon icon-sm"><use href="#es-line-add"></use></svg>
</button>
</span>
</div>
</div>
<div class="sidebar-section form-tags">
<div>
<span class="form-sidebar-items">

View file

@ -1,63 +0,0 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
frappe.provide("frappe.energy_points");
Object.assign(frappe.energy_points, {
get_points(points) {
return `<span class="bold" style="color: ${points >= 0 ? "#45A163" : "#e42121"}">
${points > 0 ? "+" : ""}${points}
</span>`;
},
format_form_log(log) {
const separator = `<span>&nbsp;-&nbsp;</span>`;
return `<span>
<!--${this.get_points(log.points)}&nbsp;-->
<a href="/app/energy-point-log/${log.name}">${this.get_form_log_message(log)}</a>
${log.reason ? separator + log.reason : ""}
</span>`;
},
format_history_log(log) {
// redundant code to honor readability and to avoid confusion
const separator = `<span>&nbsp;-&nbsp;</span>`;
const route = frappe.utils.get_form_link(log.reference_doctype, log.reference_name);
return `<div class="flex">
<span class="${log.points >= 0 ? "green" : "red"} mr-2">
${this.get_points(log.points)}
</span>
<a href="${route}" class="text-muted">${this.get_history_log_message(log)}</a>
${log.reason ? separator + log.reason : ""}
${separator + frappe.datetime.comment_when(log.creation)}
</div>`;
},
get_history_log_message(log) {
const owner_name = frappe.user.full_name(log.owner).bold();
const ref_doc = log.reference_name;
if (log.type === "Appreciation") {
return __("{0} appreciated on {1}", [owner_name, ref_doc]);
}
if (log.type === "Criticism") {
return __("{0} criticized on {1}", [owner_name, ref_doc]);
}
if (log.type === "Revert") {
return __("{0} reverted {1}", [owner_name, log.revert_of]);
}
return __("via automatic rule {0} on {1}", [log.rule.bold(), ref_doc]);
},
get_form_log_message(log) {
// redundant code to honor readability and to avoid confusion
const owner_name = frappe.user.full_name(log.owner).bold();
const user = frappe.user.full_name(log.user).bold();
if (log.type === "Appreciation") {
return __("{0} appreciated {1}", [owner_name, user]);
}
if (log.type === "Criticism") {
return __("{0} criticized {1}", [owner_name, user]);
}
if (log.type === "Revert") {
return __("{0} reverted {1}", [owner_name, log.revert_of]);
}
return __("gained by {0} via automatic rule {1}", [user, log.rule.bold()]);
},
});

View file

@ -1 +0,0 @@
import "../../desk/page/user_profile/user_profile_controller";

View file

@ -866,60 +866,6 @@ body {
}
}
.form-reviews {
.reviews {
display: flex;
flex-wrap: wrap;
}
.review {
display: flex;
font-weight: 500;
height: 28px;
border-radius: 14px;
font-size: var(--text-xs);
margin-bottom: var(--margin-sm);
margin-right: var(--margin-xs);
border: 1px solid var(--dark-border-color);
padding: 2px 3px;
align-items: center;
min-width: 60px;
background: var(--fg-color);
.avatar {
width: 20px;
height: 20px;
}
.review-points {
margin-left: 3px;
flex: 1;
text-align: center;
}
}
}
.review-popover {
padding: 0px;
min-width: 200px;
max-width: 250px;
.popover-body,
.popover-content {
padding: 0;
}
.body {
border-bottom: 1px solid $border-color;
}
.subject,
.body {
padding: var(--padding-sm);
overflow-wrap: break-word;
p {
margin-top: var(--margin-xs);
margin-bottom: 0px;
}
}
}
.liked-by-popover {
.popover-body {
min-height: 30px;

View file

@ -1,50 +0,0 @@
// Copyright (c) 2018, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on("Energy Point Log", {
refresh: function (frm) {
frm.events.make_reference_name_link(frm);
if (frm.doc.reverted) {
frm.set_intro(__("This document has been reverted"));
} else if (frm.doc.type === "Auto" && frappe.user_roles.includes("System Manager")) {
frm.add_custom_button(__("Revert"), () => frm.events.show_revert_dialog(frm));
}
},
show_revert_dialog(frm) {
const revert_dialog = new frappe.ui.Dialog({
title: __("Revert"),
fields: [
{
fieldname: "reason",
fieldtype: "Small Text",
label: __("Reason"),
reqd: 1,
},
],
primary_action: (values) => {
return frm
.call("revert", {
reason: values.reason,
})
.then((res) => {
let revert_log = res.message;
revert_dialog.hide();
revert_dialog.clear();
frappe.model.docinfo[frm.doc.reference_doctype][
frm.doc.reference_name
].energy_point_logs.unshift(revert_log);
frm.refresh();
});
},
primary_action_label: __("Submit"),
});
revert_dialog.show();
},
make_reference_name_link(frm) {
let dt = frm.doc.reference_doctype;
let dn = frm.doc.reference_name;
frm.fields_dict.reference_name.$input_wrapper
.find(".control-value")
.wrapInner(`<a href='/app/${frappe.router.slug(dt)}/${dn}'></a>`);
},
});

View file

@ -1,134 +0,0 @@
{
"actions": [],
"creation": "2018-06-21 14:58:55.913619",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"user",
"type",
"points",
"rule",
"column_break_5",
"reference_doctype",
"reference_name",
"reverted",
"revert_of",
"section_break_10",
"reason",
"seen"
],
"fields": [
{
"fieldname": "user",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "User",
"options": "User",
"read_only": 1,
"reqd": 1,
"search_index": 1
},
{
"fieldname": "type",
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Type",
"options": "Auto\nAppreciation\nCriticism\nReview\nRevert",
"read_only": 1
},
{
"fieldname": "rule",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Rule",
"options": "Energy Point Rule",
"read_only": 1
},
{
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Reference Document Type",
"options": "DocType",
"read_only": 1
},
{
"fieldname": "reference_name",
"fieldtype": "Data",
"label": "Reference Name",
"read_only": 1,
"search_index": 1
},
{
"fieldname": "points",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Points",
"read_only": 1
},
{
"fieldname": "reason",
"fieldtype": "Text",
"in_list_view": 1,
"label": "Reason",
"read_only": 1
},
{
"default": "0",
"fieldname": "reverted",
"fieldtype": "Check",
"hidden": 1,
"label": "Reverted"
},
{
"depends_on": "eval:doc.type === 'Revert'",
"fieldname": "revert_of",
"fieldtype": "Link",
"label": "Revert Of",
"options": "Energy Point Log",
"read_only": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "seen",
"fieldtype": "Check",
"hidden": 1,
"ignore_user_permissions": 1,
"label": "Seen"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-03-23 16:03:25.006938",
"modified_by": "Administrator",
"module": "Social",
"name": "Energy Point Log",
"owner": "Administrator",
"permissions": [
{
"read": 1,
"report": 1,
"role": "System Manager"
},
{
"read": 1,
"report": 1,
"role": "Desk User"
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "user"
}

View file

@ -1,391 +0,0 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# License: MIT. See LICENSE
import json
import frappe
from frappe import _
from frappe.desk.doctype.notification_log.notification_log import (
enqueue_create_notification,
get_title,
get_title_html,
)
from frappe.desk.doctype.notification_settings.notification_settings import (
is_email_notifications_enabled,
is_email_notifications_enabled_for_type,
)
from frappe.model.document import Document
from frappe.utils import cint, get_fullname, get_link_to_form, getdate
class EnergyPointLog(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
points: DF.Int
reason: DF.Text | None
reference_doctype: DF.Link | None
reference_name: DF.Data | None
revert_of: DF.Link | None
reverted: DF.Check
rule: DF.Link | None
seen: DF.Check
type: DF.Literal["Auto", "Appreciation", "Criticism", "Review", "Revert"]
user: DF.Link
# end: auto-generated types
def validate(self):
self.map_milestone_reference()
if self.type in ["Appreciation", "Criticism"] and self.user == self.owner:
frappe.throw(_("You cannot give review points to yourself"))
def map_milestone_reference(self):
# link energy point to the original reference, if set by milestone
if self.reference_doctype == "Milestone":
self.reference_doctype, self.reference_name = frappe.db.get_value(
"Milestone", self.reference_name, ["reference_type", "reference_name"]
)
def after_insert(self):
alert_dict = get_alert_dict(self)
if alert_dict:
frappe.publish_realtime(
"energy_point_alert", message=alert_dict, user=self.user, after_commit=True
)
frappe.cache.hdel("energy_points", self.user)
if self.type != "Review" and frappe.get_cached_value(
"Notification Settings", self.user, "energy_points_system_notifications"
):
reference_user = self.user if self.type == "Auto" else self.owner
notification_doc = {
"type": "Energy Point",
"document_type": self.reference_doctype,
"document_name": self.reference_name,
"subject": get_notification_message(self),
"from_user": reference_user,
"email_content": f"<div>{self.reason}</div>" if self.reason else None,
}
enqueue_create_notification(self.user, notification_doc)
def on_trash(self):
if self.type == "Revert":
reference_log = frappe.get_doc("Energy Point Log", self.revert_of)
reference_log.reverted = 0
reference_log.save()
@frappe.whitelist()
def revert(self, reason, ignore_permissions=False):
if not ignore_permissions:
frappe.only_for("System Manager")
if self.type != "Auto":
frappe.throw(_("This document cannot be reverted"))
if self.get("reverted"):
return
self.reverted = 1
self.save(ignore_permissions=True)
return frappe.get_doc(
{
"doctype": "Energy Point Log",
"points": -(self.points),
"type": "Revert",
"user": self.user,
"reason": reason,
"reference_doctype": self.reference_doctype,
"reference_name": self.reference_name,
"revert_of": self.name,
}
).insert(ignore_permissions=True)
def get_notification_message(doc):
owner_name = get_fullname(doc.owner)
points = doc.points
title = get_title(doc.reference_doctype, doc.reference_name)
if doc.type == "Auto":
owner_name = frappe.bold("You")
if points == 1:
message = _("{0} gained {1} point for {2} {3}")
else:
message = _("{0} gained {1} points for {2} {3}")
message = message.format(owner_name, frappe.bold(points), doc.rule, get_title_html(title))
elif doc.type == "Appreciation":
if points == 1:
message = _("{0} appreciated your work on {1} with {2} point")
else:
message = _("{0} appreciated your work on {1} with {2} points")
message = message.format(frappe.bold(owner_name), get_title_html(title), frappe.bold(points))
elif doc.type == "Criticism":
if points == 1:
message = _("{0} criticized your work on {1} with {2} point")
else:
message = _("{0} criticized your work on {1} with {2} points")
message = message.format(frappe.bold(owner_name), get_title_html(title), frappe.bold(points))
elif doc.type == "Revert":
if points == 1:
message = _("{0} reverted your point on {1}")
else:
message = _("{0} reverted your points on {1}")
message = message.format(frappe.bold(owner_name), get_title_html(title))
return message
def get_alert_dict(doc):
alert_dict = frappe._dict()
owner_name = get_fullname(doc.owner)
if doc.reference_doctype:
doc_link = get_link_to_form(doc.reference_doctype, doc.reference_name)
points = doc.points
bold_points = frappe.bold(doc.points)
if doc.type == "Auto":
if points == 1:
message = _("You gained {0} point")
else:
message = _("You gained {0} points")
alert_dict.message = message.format(bold_points)
alert_dict.indicator = "green"
elif doc.type == "Appreciation":
if points == 1:
message = _("{0} appreciated your work on {1} with {2} point")
else:
message = _("{0} appreciated your work on {1} with {2} points")
alert_dict.message = message.format(owner_name, doc_link, bold_points)
alert_dict.indicator = "green"
elif doc.type == "Criticism":
if points == 1:
message = _("{0} criticized your work on {1} with {2} point")
else:
message = _("{0} criticized your work on {1} with {2} points")
alert_dict.message = message.format(owner_name, doc_link, bold_points)
alert_dict.indicator = "red"
elif doc.type == "Revert":
if points == 1:
message = _("{0} reverted your point on {1}")
else:
message = _("{0} reverted your points on {1}")
alert_dict.message = message.format(
owner_name,
doc_link,
)
alert_dict.indicator = "red"
return alert_dict
def create_energy_points_log(ref_doctype, ref_name, doc, apply_only_once=False):
doc = frappe._dict(doc)
if doc.rule:
log_exists = check_if_log_exists(
ref_doctype, ref_name, doc.rule, None if apply_only_once else doc.user
)
if log_exists:
return frappe.get_doc("Energy Point Log", log_exists)
new_log = frappe.new_doc("Energy Point Log")
new_log.reference_doctype = ref_doctype
new_log.reference_name = ref_name
new_log.update(doc)
new_log.insert(ignore_permissions=True)
return new_log
def check_if_log_exists(ref_doctype, ref_name, rule, user=None):
"""'Checks if Energy Point Log already exists"""
filters = frappe._dict(
{"rule": rule, "reference_doctype": ref_doctype, "reference_name": ref_name, "reverted": 0}
)
if user:
filters.user = user
return frappe.db.exists("Energy Point Log", filters)
def create_review_points_log(user, points, reason=None, doctype=None, docname=None):
return frappe.get_doc(
{
"doctype": "Energy Point Log",
"points": points,
"type": "Review",
"user": user,
"reason": reason,
"reference_doctype": doctype,
"reference_name": docname,
}
).insert(ignore_permissions=True)
@frappe.whitelist()
def add_review_points(user, points):
frappe.only_for("System Manager")
create_review_points_log(user, points)
@frappe.whitelist()
def get_energy_points(user):
points = get_user_energy_and_review_points(user)
return frappe._dict(points.get(user, {}))
@frappe.whitelist()
def get_user_energy_and_review_points(user=None, from_date=None, as_dict=True):
conditions = ""
given_points_condition = ""
values = frappe._dict()
if user:
conditions = "WHERE `user` = %(user)s"
values.user = user
if from_date:
conditions += "WHERE" if not conditions else "AND"
given_points_condition += "AND `creation` >= %(from_date)s"
conditions += " `creation` >= %(from_date)s OR `type`='Review'"
values.from_date = from_date
points_list = frappe.db.sql(
f"""
SELECT
SUM(CASE WHEN `type` != 'Review' THEN `points` ELSE 0 END) AS energy_points,
SUM(CASE WHEN `type` = 'Review' THEN `points` ELSE 0 END) AS review_points,
SUM(CASE
WHEN `type`='Review' AND `points` < 0 {given_points_condition}
THEN ABS(`points`)
ELSE 0
END) as given_points,
`user`
FROM `tabEnergy Point Log`
{conditions}
GROUP BY `user`
ORDER BY `energy_points` DESC
""",
values=values,
as_dict=1,
)
if not as_dict:
return points_list
dict_to_return = frappe._dict()
for d in points_list:
dict_to_return[d.pop("user")] = d
return dict_to_return
@frappe.whitelist()
def review(doc, points, to_user, reason, review_type="Appreciation"):
current_review_points = get_energy_points(frappe.session.user).review_points
doc = doc.as_dict() if hasattr(doc, "as_dict") else frappe._dict(json.loads(doc))
points = abs(cint(points))
if current_review_points < points:
frappe.msgprint(_("You do not have enough review points"))
return
review_doc = create_energy_points_log(
doc.doctype,
doc.name,
{
"type": review_type,
"reason": reason,
"points": points if review_type == "Appreciation" else -points,
"user": to_user,
},
)
# deduct review points from reviewer
create_review_points_log(
user=frappe.session.user,
points=-points,
reason=reason,
doctype=review_doc.doctype,
docname=review_doc.name,
)
return review_doc
@frappe.whitelist()
def get_reviews(doctype, docname):
return frappe.get_all(
"Energy Point Log",
filters={
"reference_doctype": doctype,
"reference_name": docname,
"type": ["in", ("Appreciation", "Criticism")],
},
fields=["points", "owner", "type", "user", "reason", "creation"],
)
def send_weekly_summary():
send_summary("Weekly")
def send_monthly_summary():
send_summary("Monthly")
def send_summary(timespan):
from frappe.social.doctype.energy_point_settings.energy_point_settings import (
is_energy_point_enabled,
)
from frappe.utils.user import get_enabled_system_users
if not is_energy_point_enabled():
return
if not is_email_notifications_enabled_for_type(frappe.session.user, "Energy Point"):
return
from_date = frappe.utils.add_to_date(None, weeks=-1)
if timespan == "Monthly":
from_date = frappe.utils.add_to_date(None, months=-1)
user_points = get_user_energy_and_review_points(from_date=from_date, as_dict=False)
# do not send report if no activity found
if not user_points or not user_points[0].energy_points:
return
from_date = getdate(from_date)
to_date = getdate()
# select only those users that have energy point email notifications enabled
all_users = [
user.email
for user in get_enabled_system_users()
if is_email_notifications_enabled_for_type(user.name, "Energy Point")
]
frappe.sendmail(
subject=f"{timespan} energy points summary",
recipients=all_users,
template="energy_points_summary",
args={
"top_performer": user_points[0],
"top_reviewer": max(user_points, key=lambda x: x["given_points"]),
"standings": user_points[:10], # top 10
"footer_message": get_footer_message(timespan).format(from_date, to_date),
},
with_container=1,
)
def get_footer_message(timespan):
if timespan == "Monthly":
return _("Stats based on last month's performance (from {0} to {1})")
else:
return _("Stats based on last week's performance (from {0} to {1})")

View file

@ -1,29 +0,0 @@
frappe.listview_settings["Energy Point Log"] = {
hide_name_column: true,
add_fields: ["type", "reference_doctype", "reference_name"],
get_indicator: function (doc) {
let colors = {
Appreciation: "green",
Criticism: "red",
Auto: "blue",
Revert: "orange",
Review: "grey",
};
return [__(doc.type), colors[doc.type], "type,=," + doc.type];
},
button: {
show: function (doc) {
return doc.reference_name;
},
get_label: function () {
return __("View Ref");
},
get_description: function (doc) {
return __("Open {0}", [`${doc.reference_doctype} ${doc.reference_name}`]);
},
action: function (doc) {
frappe.set_route("Form", doc.reference_doctype, doc.reference_name);
},
},
};

View file

@ -1,389 +0,0 @@
# Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE
from unittest.case import skipIf
import frappe
from frappe.cache_manager import clear_doctype_map
from frappe.desk.form.assign_to import add as assign_to
from frappe.desk.page.user_profile.user_profile import get_energy_points_heatmap_data
from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.utils.testutils import add_custom_field, clear_custom_fields
from .energy_point_log import create_review_points_log, review
from .energy_point_log import get_energy_points as _get_energy_points
class UnitTestEnergyPointLog(UnitTestCase):
"""
Unit tests for EnergyPointLog.
Use this class for testing individual functions and methods.
"""
pass
class TestEnergyPointLog(IntegrationTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
settings = frappe.get_single("Energy Point Settings")
settings.enabled = 1
settings.save()
@classmethod
def tearDownClass(cls):
settings = frappe.get_single("Energy Point Settings")
settings.enabled = 0
settings.save()
def setUp(self):
clear_doctype_map("Energy Point Rule")
def tearDown(self):
frappe.set_user("Administrator")
frappe.db.delete("Energy Point Log")
frappe.db.delete("Energy Point Rule")
clear_doctype_map("Energy Point Rule")
def test_user_energy_point(self):
frappe.set_user("test@example.com")
todo_point_rule = create_energy_point_rule_for_todo()
energy_point_of_user = get_points("test@example.com")
created_todo = create_a_todo()
created_todo.status = "Closed"
created_todo.save()
points_after_closing_todo = get_points("test@example.com")
self.assertEqual(points_after_closing_todo, energy_point_of_user + todo_point_rule.points)
created_todo.save()
points_after_double_save = get_points("test@example.com")
# point should not be awarded more than once for same doc
self.assertEqual(points_after_double_save, energy_point_of_user + todo_point_rule.points)
def test_points_based_on_multiplier_field(self):
frappe.set_user("test@example.com")
add_custom_field("ToDo", "multiplier", "Float")
multiplier_value = 0.51
todo_point_rule = create_energy_point_rule_for_todo("multiplier")
energy_point_of_user = get_points("test@example.com")
created_todo = create_a_todo()
created_todo.status = "Closed"
created_todo.multiplier = multiplier_value
created_todo.save()
points_after_closing_todo = get_points("test@example.com")
self.assertEqual(
points_after_closing_todo,
energy_point_of_user + round(todo_point_rule.points * multiplier_value),
)
clear_custom_fields("ToDo")
def test_points_based_on_max_points(self):
frappe.set_user("test@example.com")
# here multiplier is high
# let see if points get capped to max_point limit
multiplier_value = 15
max_points = 50
add_custom_field("ToDo", "multiplier", "Float")
todo_point_rule = create_energy_point_rule_for_todo("multiplier", max_points=max_points)
energy_point_of_user = get_points("test@example.com")
created_todo = create_a_todo()
created_todo.status = "Closed"
created_todo.multiplier = multiplier_value
created_todo.save()
points_after_closing_todo = get_points("test@example.com")
# test max_points cap
self.assertNotEqual(
points_after_closing_todo,
energy_point_of_user + round(todo_point_rule.points * multiplier_value),
)
self.assertEqual(points_after_closing_todo, energy_point_of_user + max_points)
clear_custom_fields("ToDo")
def test_disabled_energy_points(self):
settings = frappe.get_single("Energy Point Settings")
settings.enabled = 0
settings.save()
frappe.set_user("test@example.com")
create_energy_point_rule_for_todo()
energy_point_of_user = get_points("test@example.com")
created_todo = create_a_todo()
created_todo.status = "Closed"
created_todo.save()
points_after_closing_todo = get_points("test@example.com")
# no change in points
self.assertEqual(points_after_closing_todo, energy_point_of_user)
settings.enabled = 1
settings.save()
def test_review(self):
created_todo = create_a_todo()
review_points = 20
create_review_points_log("test2@example.com", review_points)
# reviewer
frappe.set_user("test2@example.com")
review_points_before_review = get_points("test2@example.com", "review_points")
self.assertEqual(review_points_before_review, review_points)
# for appreciation
appreciation_points = 5
energy_points_before_review = get_points("test@example.com")
review(created_todo, appreciation_points, "test@example.com", "good job")
energy_points_after_review = get_points("test@example.com")
review_points_after_review = get_points("test2@example.com", "review_points")
self.assertEqual(energy_points_after_review, energy_points_before_review + appreciation_points)
self.assertEqual(review_points_after_review, review_points_before_review - appreciation_points)
# for criticism
criticism_points = 2
todo = create_a_todo(description="Bad patch")
energy_points_before_review = energy_points_after_review
review_points_before_review = review_points_after_review
review(todo, criticism_points, "test@example.com", "You could have done better.", "Criticism")
energy_points_after_review = get_points("test@example.com")
review_points_after_review = get_points("test2@example.com", "review_points")
self.assertEqual(energy_points_after_review, energy_points_before_review - criticism_points)
self.assertEqual(review_points_after_review, review_points_before_review - criticism_points)
def test_user_energy_point_as_admin(self):
frappe.set_user("Administrator")
create_energy_point_rule_for_todo()
created_todo = create_a_todo()
created_todo.status = "Closed"
created_todo.save()
points_after_closing_todo = get_points("Administrator")
# no points for admin
self.assertEqual(points_after_closing_todo, 0)
def test_revert_points_on_cancelled_doc(self):
frappe.set_user("test@example.com")
create_energy_point_rule_for_todo()
created_todo = create_a_todo()
created_todo.status = "Closed"
created_todo.save()
energy_point_logs = frappe.get_all("Energy Point Log")
self.assertEqual(len(energy_point_logs), 1)
# for submit and cancel permission
frappe.set_user("Administrator")
# submit
created_todo.docstatus = 1
created_todo.save()
# cancel
created_todo.docstatus = 2
created_todo.save()
energy_point_logs = frappe.get_all("Energy Point Log", fields=["reference_name", "type", "reverted"])
self.assertListEqual(
energy_point_logs,
[
{"reference_name": created_todo.name, "type": "Revert", "reverted": 0},
{"reference_name": created_todo.name, "type": "Auto", "reverted": 1},
],
)
def test_energy_point_for_new_document_creation(self):
frappe.set_user("test@example.com")
todo_point_rule = create_energy_point_rule_for_todo(for_doc_event="New")
points_before_todo_creation = get_points("test@example.com")
create_a_todo()
points_after_todo_creation = get_points("test@example.com")
self.assertEqual(points_after_todo_creation, points_before_todo_creation + todo_point_rule.points)
def test_point_allocation_for_assigned_users(self):
todo = create_a_todo()
assign_users_to_todo(todo.name, ["test@example.com", "test2@example.com"])
test_user_before_points = get_points("test@example.com")
test2_user_before_points = get_points("test2@example.com")
rule = create_energy_point_rule_for_todo(for_assigned_users=1)
todo.status = "Closed"
todo.save()
test_user_after_points = get_points("test@example.com")
test2_user_after_points = get_points("test2@example.com")
self.assertEqual(test_user_after_points, test_user_before_points + rule.points)
self.assertEqual(test2_user_after_points, test2_user_before_points + rule.points)
@skipIf(
frappe.conf.db_type == "sqlite",
"Not for SQLite for now",
)
def test_eps_heatmap_query(self):
# Just asserts that query works, not correctness.
self.assertIsInstance(get_energy_points_heatmap_data(user="test@example.com", date=None), dict)
def test_points_on_field_value_change(self):
rule = create_energy_point_rule_for_todo(for_doc_event="Value Change", field_to_check="description")
frappe.set_user("test@example.com")
points_before_todo_creation = get_points("test@example.com")
todo = create_a_todo()
todo.status = "Closed"
todo.save()
points_after_closing_todo = get_points("test@example.com")
self.assertEqual(points_after_closing_todo, points_before_todo_creation)
todo.description = "This is new todo"
todo.save()
points_after_changing_todo_description = get_points("test@example.com")
self.assertEqual(points_after_changing_todo_description, points_before_todo_creation + rule.points)
def test_apply_only_once(self):
frappe.set_user("test@example.com")
todo_point_rule = create_energy_point_rule_for_todo(apply_once=True, user_field="modified_by")
first_user_points = get_points("test@example.com")
created_todo = create_a_todo()
created_todo.status = "Closed"
created_todo.save()
first_user_points_after_closing_todo = get_points("test@example.com")
self.assertEqual(first_user_points_after_closing_todo, first_user_points + todo_point_rule.points)
frappe.set_user("test2@example.com")
second_user_points = get_points("test2@example.com")
created_todo.save(ignore_permissions=True)
second_user_points_after_closing_todo = get_points("test2@example.com")
# point should not be awarded more than once for same doc (irrespective of user)
self.assertEqual(second_user_points_after_closing_todo, second_user_points)
def test_allow_creation_of_new_log_if_the_previous_log_was_reverted(self):
frappe.set_user("test@example.com")
todo_point_rule = create_energy_point_rule_for_todo()
energy_point_of_user = get_points("test@example.com")
created_todo = create_a_todo()
created_todo.status = "Closed"
created_todo.save()
points_after_closing_todo = get_points("test@example.com")
log_name = frappe.db.exists("Energy Point Log", {"reference_name": created_todo.name})
frappe.get_doc("Energy Point Log", log_name).revert("Just for test")
points_after_reverting_todo = get_points("test@example.com")
created_todo.save()
points_after_saving_todo_again = get_points("test@example.com")
rule_points = todo_point_rule.points
self.assertEqual(points_after_closing_todo, energy_point_of_user + rule_points)
self.assertEqual(points_after_reverting_todo, points_after_closing_todo - rule_points)
self.assertEqual(points_after_saving_todo_again, points_after_reverting_todo + rule_points)
def test_energy_points_disabled_user(self):
frappe.set_user("test@example.com")
user = frappe.get_doc("User", "test@example.com")
user.enabled = 0
user.save()
todo_point_rule = create_energy_point_rule_for_todo()
energy_point_of_user = get_points("test@example.com")
created_todo = create_a_todo()
created_todo.status = "Closed"
created_todo.save()
points_after_closing_todo = get_points("test@example.com")
# do not update energy points for disabled user
self.assertEqual(points_after_closing_todo, energy_point_of_user)
with self.set_user("Administrator"):
user.enabled = 1
user.save()
created_todo.save()
points_after_re_saving_todo = get_points("test@example.com")
self.assertEqual(points_after_re_saving_todo, energy_point_of_user + todo_point_rule.points)
def create_energy_point_rule_for_todo(
multiplier_field=None,
for_doc_event="Custom",
max_points=None,
for_assigned_users=0,
field_to_check=None,
apply_once=False,
user_field="owner",
):
name = "ToDo Closed"
point_rule_exists = frappe.db.exists("Energy Point Rule", name)
if point_rule_exists:
return frappe.get_doc("Energy Point Rule", name)
return frappe.get_doc(
{
"doctype": "Energy Point Rule",
"rule_name": name,
"points": 5,
"reference_doctype": "ToDo",
"condition": 'doc.status == "Closed"',
"for_doc_event": for_doc_event,
"user_field": user_field,
"for_assigned_users": for_assigned_users,
"multiplier_field": multiplier_field,
"max_points": max_points,
"field_to_check": field_to_check,
"apply_only_once": apply_once,
}
).insert(ignore_permissions=1)
def create_a_todo(description=None):
if not description:
description = "Fix a bug"
return frappe.get_doc(
{
"doctype": "ToDo",
"description": description,
}
).insert(ignore_permissions=True)
def get_points(user, point_type="energy_points"):
return _get_energy_points(user).get(point_type, 0)
def assign_users_to_todo(todo_name, users):
for user in users:
assign_to({"assign_to": [user], "doctype": "ToDo", "name": todo_name})

View file

@ -1,58 +0,0 @@
// Copyright (c) 2018, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on("Energy Point Rule", {
validate(frm) {
frm.set_df_property("user_field", "reqd", !frm.doc.for_assigned_users);
frm.set_df_property("condition", "reqd", frm.doc.for_doc_event === "Custom");
},
refresh(frm) {
frm.events.set_field_options(frm);
},
for_doc_event(frm) {
if (frm.doc.for_assigned_users) {
frm.set_value("for_assigned_users", !frm.doc.for_doc_event === "New");
}
},
reference_doctype(frm) {
frm.events.set_field_options(frm);
},
set_field_options(frm) {
// sets options for field_to_check, user_field and multiplier fields
// based on reference doctype
const reference_doctype = frm.doc.reference_doctype;
if (!reference_doctype) return;
frappe.model.with_doctype(reference_doctype, () => {
const map_for_options = (df) => ({ label: df.label, value: df.fieldname });
const fields = frappe.meta
.get_docfields(frm.doc.reference_doctype)
.filter(frappe.model.is_value_type);
const fields_to_check = fields.map(map_for_options);
const user_fields = fields
.filter(
(df) =>
(df.fieldtype === "Link" && df.options === "User") ||
df.fieldtype === "Data"
)
.map(map_for_options)
.concat([
{ label: __("Owner"), value: "owner" },
{ label: __("Modified By"), value: "modified_by" },
]);
const multiplier_fields = fields
.filter((df) => ["Int", "Float"].includes(df.fieldtype))
.map(map_for_options);
// blank option for the ability to unset the multiplier field
multiplier_fields.unshift(null);
frm.set_df_property("field_to_check", "options", fields_to_check);
frm.set_df_property("user_field", "options", user_fields);
frm.set_df_property("multiplier_field", "options", multiplier_fields);
});
},
});

View file

@ -1,143 +0,0 @@
{
"actions": [],
"autoname": "field:rule_name",
"creation": "2018-06-20 10:08:36.275253",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enabled",
"section_break_2",
"rule_name",
"reference_doctype",
"for_doc_event",
"field_to_check",
"points",
"for_assigned_users",
"user_field",
"multiplier_field",
"max_points",
"column_break_12",
"apply_only_once",
"condition"
],
"fields": [
{
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Enabled",
"reqd": 1
},
{
"fieldname": "rule_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Rule Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Reference Document Type",
"options": "DocType",
"reqd": 1
},
{
"depends_on": "eval:['Custom', 'Value Change'].includes(doc.for_doc_event)",
"description": "If the condition is satisfied user will be rewarded with the points. eg. doc.status == 'Closed'\n",
"fieldname": "condition",
"fieldtype": "Code",
"in_list_view": 1,
"label": "Condition"
},
{
"fieldname": "points",
"fieldtype": "Int",
"label": "Points",
"reqd": 1
},
{
"depends_on": "eval:!doc.for_assigned_users || doc.for_doc_event==='New'",
"description": "The user from this field will be rewarded points",
"fieldname": "user_field",
"fieldtype": "Select",
"label": "User Field"
},
{
"fieldname": "multiplier_field",
"fieldtype": "Select",
"label": "Multiplier Field"
},
{
"depends_on": "eval:doc.multiplier_field",
"description": "Maximum points allowed after multiplying points with the multiplier value\n(Note: For no limit leave this field empty or set 0)",
"fieldname": "max_points",
"fieldtype": "Int",
"label": "Maximum Points"
},
{
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"default": "Custom",
"fieldname": "for_doc_event",
"fieldtype": "Select",
"label": "For Document Event",
"options": "New\nSubmit\nCancel\nValue Change\nCustom"
},
{
"default": "0",
"depends_on": "eval:doc.for_doc_event !=='New'",
"description": "Users assigned to the reference document will get points.",
"fieldname": "for_assigned_users",
"fieldtype": "Check",
"label": "Allot Points To Assigned Users"
},
{
"depends_on": "eval:doc.for_doc_event=='Value Change'",
"fieldname": "field_to_check",
"fieldtype": "Select",
"label": "Field To Check"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Apply this rule only once per document",
"fieldname": "apply_only_once",
"fieldtype": "Check",
"label": "Apply Only Once"
}
],
"links": [],
"modified": "2024-03-23 16:03:25.142267",
"modified_by": "Administrator",
"module": "Social",
"name": "Energy Point Rule",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -1,158 +0,0 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# License: MIT. See LICENSE
import frappe
import frappe.cache_manager
from frappe import _
from frappe.core.doctype.user.user import get_enabled_users
from frappe.model import log_types
from frappe.model.document import Document
from frappe.social.doctype.energy_point_log.energy_point_log import create_energy_points_log
from frappe.social.doctype.energy_point_settings.energy_point_settings import (
is_energy_point_enabled,
)
class EnergyPointRule(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
apply_only_once: DF.Check
condition: DF.Code | None
enabled: DF.Check
field_to_check: DF.Literal[None]
for_assigned_users: DF.Check
for_doc_event: DF.Literal["New", "Submit", "Cancel", "Value Change", "Custom"]
max_points: DF.Int
multiplier_field: DF.Literal[None]
points: DF.Int
reference_doctype: DF.Link
rule_name: DF.Data
user_field: DF.Literal[None]
# end: auto-generated types
def on_update(self):
frappe.cache_manager.clear_doctype_map("Energy Point Rule", self.reference_doctype)
def on_trash(self):
frappe.cache_manager.clear_doctype_map("Energy Point Rule", self.reference_doctype)
def apply(self, doc):
if self.rule_condition_satisfied(doc):
multiplier = 1
points = self.points
if self.multiplier_field:
multiplier = doc.get(self.multiplier_field) or 1
points = round(points * multiplier)
max_points = self.max_points
if max_points and points > max_points:
points = max_points
reference_doctype = doc.doctype
reference_name = doc.name
users = []
if self.for_assigned_users:
users = doc.get_assigned_users()
else:
users = [doc.get(self.user_field)]
rule = self.name
# incase of zero as result after roundoff
if not points:
return
try:
for user in users:
if not is_eligible_user(user):
continue
create_energy_points_log(
reference_doctype,
reference_name,
{"points": points, "user": user, "rule": rule},
self.apply_only_once,
)
except Exception:
self.log_error("Energy points failed")
def rule_condition_satisfied(self, doc):
if self.for_doc_event == "New":
# indicates that this was a new doc
return doc.get_doc_before_save() is None
if self.for_doc_event == "Submit":
return doc.docstatus.is_submitted()
if self.for_doc_event == "Cancel":
return doc.docstatus.is_cancelled()
if self.for_doc_event == "Value Change":
field_to_check = self.field_to_check
if not field_to_check:
return False
doc_before_save = doc.get_doc_before_save()
# check if the field has been changed
# if condition is set check if it is satisfied
return (
doc_before_save
and doc_before_save.get(field_to_check) != doc.get(field_to_check)
and (not self.condition or self.eval_condition(doc))
)
if self.for_doc_event == "Custom" and self.condition:
return self.eval_condition(doc)
return False
def eval_condition(self, doc):
return self.condition and frappe.safe_eval(self.condition, None, {"doc": doc.as_dict()})
def process_energy_points(doc, state):
if (
frappe.flags.in_patch
or frappe.flags.in_install
or frappe.flags.in_migrate
or frappe.flags.in_import
or frappe.flags.in_setup_wizard
or doc.doctype in log_types
):
return
if not is_energy_point_enabled():
return
old_doc = doc.get_doc_before_save()
# check if doc has been cancelled
if old_doc and old_doc.docstatus.is_submitted() and doc.docstatus.is_cancelled():
return revert_points_for_cancelled_doc(doc)
for d in frappe.cache_manager.get_doctype_map(
"Energy Point Rule", doc.doctype, dict(reference_doctype=doc.doctype, enabled=1)
):
frappe.get_doc("Energy Point Rule", d.get("name")).apply(doc)
def revert_points_for_cancelled_doc(doc):
energy_point_logs = frappe.get_all(
"Energy Point Log",
{"reference_doctype": doc.doctype, "reference_name": doc.name, "type": "Auto"},
)
for log in energy_point_logs:
reference_log = frappe.get_doc("Energy Point Log", log.name)
reference_log.revert(_("Reference document has been cancelled"), ignore_permissions=True)
def get_energy_point_doctypes():
return [
d.reference_doctype
for d in frappe.get_all("Energy Point Rule", ["reference_doctype"], {"enabled": 1})
]
def is_eligible_user(user):
"""Checks if user is eligible to get energy points"""
enabled_users = get_enabled_users()
return user and user in enabled_users and user != "Administrator"

View file

@ -1,50 +0,0 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on("Energy Point Settings", {
refresh: function (frm) {
if (frm.doc.enabled) {
frm.add_custom_button(__("Give Review Points"), show_review_points_dialog);
}
},
});
function show_review_points_dialog() {
const dialog = new frappe.ui.Dialog({
title: __("Give Review Points"),
fields: [
{
label: "User",
fieldname: "user",
fieldtype: "Link",
options: "User",
reqd: 1,
},
{
label: "Points",
fieldname: "points",
fieldtype: "Int",
reqd: 1,
},
],
primary_action: function (values) {
frappe
.xcall(
"frappe.social.doctype.energy_point_log.energy_point_log.add_review_points",
{
user: values.user,
points: values.points,
}
)
.then(() => {
frappe.show_alert({
message: __("Successfully Done"),
indicator: "green",
});
})
.finally(() => dialog.hide());
},
primary_action_label: __("Submit"),
});
dialog.show();
}

View file

@ -1,71 +0,0 @@
{
"actions": [],
"creation": "2019-03-19 13:17:51.710241",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enabled",
"section_break_2",
"review_levels",
"point_allocation_periodicity",
"last_point_allocation_date"
],
"fields": [
{
"default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
},
{
"depends_on": "enabled",
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"fieldname": "review_levels",
"fieldtype": "Table",
"label": "Review Levels",
"options": "Review Level"
},
{
"default": "Weekly",
"fieldname": "point_allocation_periodicity",
"fieldtype": "Select",
"label": "Point Allocation Periodicity",
"options": "Daily\nWeekly\nMonthly"
},
{
"fieldname": "last_point_allocation_date",
"fieldtype": "Date",
"label": "Last Point Allocation Date",
"read_only": 1
}
],
"hide_toolbar": 1,
"issingle": 1,
"links": [],
"modified": "2024-03-23 16:03:25.273422",
"modified_by": "Administrator",
"module": "Social",
"name": "Energy Point Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View file

@ -1,80 +0,0 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
from frappe.social.doctype.energy_point_log.energy_point_log import create_review_points_log
from frappe.utils import add_to_date, getdate, today
class EnergyPointSettings(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.social.doctype.review_level.review_level import ReviewLevel
from frappe.types import DF
enabled: DF.Check
last_point_allocation_date: DF.Date | None
point_allocation_periodicity: DF.Literal["Daily", "Weekly", "Monthly"]
review_levels: DF.Table[ReviewLevel]
# end: auto-generated types
def on_update(self):
if self.has_value_changed("enabled"):
frappe.cache.delete_key("bootinfo")
def is_energy_point_enabled():
return frappe.client_cache.get_doc("Energy Point Settings").enabled
def allocate_review_points():
settings = frappe.get_single("Energy Point Settings")
if not can_allocate_today(settings.last_point_allocation_date, settings.point_allocation_periodicity):
return
user_point_map = {}
for level in settings.review_levels:
users = get_users_with_role(level.role)
for user in users:
user_point_map.setdefault(user, 0)
# to avoid duplicate point allocation
user_point_map[user] = max([user_point_map[user], level.review_points])
for user, points in user_point_map.items():
create_review_points_log(user, points)
settings.last_point_allocation_date = today()
settings.save(ignore_permissions=True)
def can_allocate_today(last_date, periodicity):
if not last_date:
return True
days_to_add = {"Daily": 1, "Weekly": 7, "Monthly": 30}.get(periodicity, 1)
next_allocation_date = add_to_date(last_date, days=days_to_add)
return getdate(next_allocation_date) <= getdate()
def get_users_with_role(role):
return [
p[0]
for p in frappe.db.sql(
"""SELECT DISTINCT `tabUser`.`name`
FROM `tabHas Role`, `tabUser`
WHERE `tabHas Role`.`role`=%s
AND `tabUser`.`name`!='Administrator'
AND `tabHas Role`.`parent`=`tabUser`.`name`
AND `tabUser`.`enabled`=1""",
role,
)
]

View file

@ -1,18 +0,0 @@
# Copyright (c) 2021, Frappe Technologies and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
class UnitTestEnergyPointSettings(UnitTestCase):
"""
Unit tests for EnergyPointSettings.
Use this class for testing individual functions and methods.
"""
pass
class TestEnergyPointSettings(IntegrationTestCase):
pass

View file

@ -1,7 +0,0 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on("Review Level", {
// refresh: function(frm) {
// }
});

View file

@ -1,51 +0,0 @@
{
"actions": [],
"creation": "2019-03-19 13:16:12.762352",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"level_name",
"role",
"review_points"
],
"fields": [
{
"fieldname": "level_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Level Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "role",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Role",
"options": "Role",
"reqd": 1,
"unique": 1
},
{
"fieldname": "review_points",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Review Points",
"reqd": 1
}
],
"istable": 1,
"links": [],
"modified": "2024-03-23 16:03:36.743533",
"modified_by": "Administrator",
"module": "Social",
"name": "Review Level",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -1,25 +0,0 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# License: MIT. See LICENSE
# import frappe
from frappe.model.document import Document
class ReviewLevel(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
level_name: DF.Data
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
review_points: DF.Int
role: DF.Link
# end: auto-generated types
pass

View file

@ -1,54 +0,0 @@
{% from "frappe/templates/includes/avatar_macro.html" import avatar %}
{% if top_performer.energy_points %}
<h1 class="text-2xl">{{ _('Top Performer') }} 🏆 </h1>
<p>
<span class="text-muted">
{{ avatar(top_performer.user) }} &nbsp;
{{
_('{0} gained {1} points').format(
frappe.get_fullname(top_performer.user),
frappe.utils.cint(top_performer.energy_points)
)
}}
</span>
</p>
{% endif %}
{% if top_reviewer.given_points %}
<h1 class="text-xl">{{ _('Top Reviewer') }} ❤️ </h1>
<p>
<span class="text-muted">
{{ avatar(top_reviewer.user) }} &nbsp;
{{
_('{0} gave {1} points').format(
frappe.get_fullname(top_reviewer.user),
frappe.utils.cint(top_reviewer.given_points)
)
}}
</span>
</p>
{% endif %}
<h3 class="text-regular" style="margin-top: 35px">{{ _('Standings') }} ({{ _('Top {0}').format(standings|length) }}) </h3>
<table class='table table-bordered'>
<tr class="text-muted text-medium">
<td> # </td>
<td style='width: 70%'>{{ _('User') }}</td>
<td style='width: 15%'>{{ _('Energy Points') }}</td>
<td style='width: 15%'>{{ _('Points Given') }}</td>
</tr>
{% for user in standings %}
<tr class="text-small">
<td>{{ loop.index }}</td>
<td>{{ frappe.get_fullname(user.user) }}</td>
<td class="text-muted">{{ frappe.utils.cint(user.energy_points) }}</td>
<td class="text-muted">{{ frappe.utils.cint(user.given_points) }}</td>
</tr>
{% endfor %}
</table>
<p class="text-muted text-medium">
{{ footer_message }}
</p>