Skip to content

Commit

Permalink
feat: What's New
Browse files Browse the repository at this point in the history
  • Loading branch information
ankush committed Apr 17, 2024
1 parent f1c8a99 commit 6d3afef
Show file tree
Hide file tree
Showing 15 changed files with 252 additions and 8 deletions.
2 changes: 2 additions & 0 deletions frappe/boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import frappe.defaults
import frappe.desk.desk_page
from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings
from frappe.desk.doctype.changelog_feed.changelog_feed import get_changelog_feed_items
from frappe.desk.doctype.form_tour.form_tour import get_onboarding_ui_tours
from frappe.desk.doctype.route_history.route_history import frequently_visited_links
from frappe.desk.form.load import get_meta_bundle
Expand Down Expand Up @@ -107,6 +108,7 @@ def get_bootinfo():
bootinfo.translated_doctypes = get_translated_doctypes()
bootinfo.subscription_conf = add_subscription_conf()
bootinfo.marketplace_apps = get_marketplace_apps()
bootinfo.changelog_feed = get_changelog_feed_items()

return bootinfo

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def get_next_execution(self):
next_execution = croniter(self.cron_format, last_execution).get_next(datetime)

jitter = 0
if self.frequency in ("Hourly Long", "Daily Long"):
if "Long" in self.frequency:
jitter = randint(1, 600)
return next_execution + timedelta(seconds=jitter)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_weekly_job(self):
dict(method="frappe.social.doctype.energy_point_log.energy_point_log.send_weekly_summary"),
)
job.db_set("last_execution", "2019-01-01 00:00:00")
self.assertTrue(job.is_event_due(get_datetime("2019-01-06 00:00:01")))
self.assertTrue(job.is_event_due(get_datetime("2019-01-06 00:10:01"))) # +10 min because of jitter
self.assertFalse(job.is_event_due(get_datetime("2019-01-02 00:00:06")))
self.assertFalse(job.is_event_due(get_datetime("2019-01-05 23:59:59")))

Expand Down
2 changes: 1 addition & 1 deletion frappe/core/doctype/user/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ def test_rate_limiting_for_reset_password(self):
res1 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
res2 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
self.assertEqual(res1.status_code, 404)
self.assertEqual(res2.status_code, 417)
self.assertEqual(res2.status_code, 429)

def test_user_rename(self):
old_name = "test_user_rename@example.com"
Expand Down
Empty file.
8 changes: 8 additions & 0 deletions frappe/desk/doctype/changelog_feed/changelog_feed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) 2023, Frappe Technologies and contributors
// For license information, please see license.txt

// frappe.ui.form.on("Changelog Feed", {
// refresh(frm) {

// },
// });
70 changes: 70 additions & 0 deletions frappe/desk/doctype/changelog_feed/changelog_feed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"actions": [],
"allow_rename": 1,
"beta": 1,
"creation": "2023-05-16 19:37:51.047664",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"app_name",
"link",
"posting_timestamp"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "app_name",
"fieldtype": "Data",
"label": "App Name"
},
{
"fieldname": "link",
"fieldtype": "Long Text",
"in_list_view": 1,
"label": "Link",
"reqd": 1
},
{
"fieldname": "posting_timestamp",
"fieldtype": "Datetime",
"label": "Posting Timestamp",
"reqd": 1,
"search_index": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-04-08 18:36:42.203032",
"modified_by": "Administrator",
"module": "Desk",
"name": "Changelog Feed",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"read_only": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
93 changes: 93 additions & 0 deletions frappe/desk/doctype/changelog_feed/changelog_feed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Copyright (c) 2023, Frappe Technologies and contributors
# For license information, please see license.txt


import requests

import frappe
from frappe.model.document import Document
from frappe.utils.caching import redis_cache
from frappe.utils.data import add_to_date


class ChangelogFeed(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

app_name: DF.Data | None
link: DF.LongText
posting_timestamp: DF.Datetime
title: DF.Data
# end: auto-generated types

pass


def fetch_changelog_feed():
"""Fetches changelog feed items from source using `get_changelog_feed` hook and stores in the db"""
since = frappe.db.get_value(
"Changelog Feed",
filters={},
fieldname="posting_timestamp",
order_by="posting_timestamp desc",
) or add_to_date(None, months=-1, as_datetime=True, as_string=False)

for fn in frappe.get_hooks("get_changelog_feed"):
try:
cache_key = f"changelog_feed::{fn}"
changelog_feed = frappe.cache.get_value(cache_key, shared=True)
if changelog_feed is None:
changelog_feed = frappe.call(fn, since=since)[:20] or []
frappe.cache.set_value(
cache_key, changelog_feed, expires_in_sec=7 * 24 * 60 * 60, shared=True
)

for feed_item in changelog_feed:
feed = {
"title": feed_item["title"],
"app_name": feed_item["app_name"],
"link": feed_item["link"],
"posting_timestamp": feed_item["creation"],
}
if not frappe.db.exists("Changelog Feed", feed):
frappe.new_doc("Changelog Feed").update(feed).insert()
except Exception:
frappe.log_error(f"Failed to fetch changelog from {fn}")
# don't retry if it's broken for 1 week
frappe.cache.set_value(cache_key, [], expires_in_sec=7 * 24 * 60 * 60, shared=True)


@redis_cache
def get_changelog_feed_items():
"""Returns a list of latest 10 changelog feed items"""
feed = frappe.get_all(
"Changelog Feed",
fields=["title", "app_name", "link", "posting_timestamp"],
# allow pubishing feed for many apps with single hook
filters={"app_name": ("in", frappe.get_installed_apps())},
order_by="posting_timestamp desc",
limit=20,
)
for f in feed:
f["app_title"] = _app_title(f["app_name"])

return feed


def _app_title(app_name):
try:
return frappe.get_hooks("app_title", app_name=app_name)[0]
except Exception:
return app_name


def get_feed(since):
"""'What's New' feed implementation for Frappe"""
r = requests.get(f"https://frappe.io/api/method/changelog_feed?since={since}")
r.raise_for_status()
return r.json()["message"]
9 changes: 9 additions & 0 deletions frappe/desk/doctype/changelog_feed/test_changelog_feed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies and Contributors
# See license.txt

# import frappe
from frappe.tests.utils import FrappeTestCase


class TestChangelogFeed(FrappeTestCase):
pass
2 changes: 1 addition & 1 deletion frappe/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ class InvalidSignatureError(ValidationError):


class RateLimitExceededError(ValidationError):
pass
http_status_code = 429


class CannotChangeConstantError(ValidationError):
Expand Down
3 changes: 3 additions & 0 deletions frappe/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@
"frappe.desk.form.document_follow.send_weekly_updates",
"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",
Expand Down Expand Up @@ -455,6 +456,8 @@
"frappe.utils.sentry.add_bootinfo",
]

get_changelog_feed = "frappe.desk.doctype.changelog_feed.changelog_feed.get_feed"

export_python_type_annotations = True

# log doctype cleanups to automatically add in log settings
Expand Down
59 changes: 58 additions & 1 deletion frappe/public/js/frappe/ui/notifications/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ frappe.ui.Notifications = class Notifications {
this.body = this.dropdown_list.find(".notification-list-body");
this.panel_events = this.dropdown_list.find(".panel-events");
this.panel_notifications = this.dropdown_list.find(".panel-notifications");
this.panel_changelog_feed = this.dropdown_list.find(".panel-changelog-feed");

this.user = frappe.session.user;

Expand Down Expand Up @@ -52,11 +53,17 @@ frappe.ui.Notifications = class Notifications {
el: this.panel_notifications,
},
{
label: __("Today's Events"),
label: __("Events"),
id: "todays_events",
view: EventsView,
el: this.panel_events,
},
{
label: __("What's New"),
id: "changelog_feed",
view: ChangelogFeedView,
el: this.panel_changelog_feed,
},
];

let get_headers_html = (item) => {
Expand Down Expand Up @@ -439,3 +446,53 @@ class EventsView extends BaseNotificationsView {
this.container.html(html);
}
}

class ChangelogFeedView extends BaseNotificationsView {
make() {
this.render_changelog_feed_html(frappe.boot.changelog_feed || []);
}

render_changelog_feed_html(changelog_feed) {
let html = "";
if (changelog_feed.length) {
this.container.empty();
const get_changelog_feed_html = (changelog_feed_item) => {
const timestamp = frappe.datetime.prettyDate(
changelog_feed_item.posting_timestamp
);
const message_html = `<div class="message">
<div>${changelog_feed_item.title}</div>
<div class="notification-timestamp text-muted">
${changelog_feed_item.app_title} | ${timestamp}
</div>
</div>`;

const item_html = `<a class="recent-item notification-item"
href="${changelog_feed_item.link}"
data-name="${changelog_feed_item.title}"
target="_blank" rel="noopener noreferrer"
>
<div class="notification-body">
${message_html}
</div>
</div>
</a>`;

return item_html;
};
html = changelog_feed.map(get_changelog_feed_html).join("");
} else {
html = `<div class="notification-null-state">
<div class="text-center">
<img src="/assets/frappe/images/ui-states/notification-empty-state.svg" alt="Generic Empty State" class="null-state">
<div class="title">${__("Nothing New")}</div>
<div class="subtitle">
${__("There is nothing new to show you right now.")}
</div>
</div>
</div>
`;
}
this.container.html(html);
}
}
3 changes: 2 additions & 1 deletion frappe/public/js/frappe/ui/toolbar/navbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
<div class="notification-list-body">
<div class="panel-notifications"></div>
<div class="panel-events"></div>
<div class="panel-changelog-feed"></div>
</div>
</div>
</li>
Expand Down Expand Up @@ -150,4 +151,4 @@
</div>
{% endif %}

</div>
</div>
3 changes: 2 additions & 1 deletion frappe/rate_limiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ def wrapper(*args, **kwargs):
value = frappe.cache.incrby(cache_key, 1)
if value > _limit:
frappe.throw(
_("You hit the rate limit because of too many requests. Please try after sometime.")
_("You hit the rate limit because of too many requests. Please try after sometime."),
frappe.RateLimitExceededError,
)

return fn(*args, **kwargs)
Expand Down
2 changes: 1 addition & 1 deletion frappe/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def test_login_with_email_link(self):
# Rate limiting
for _ in range(6):
res = requests.get(_generate_temporary_login_link(user, 10))
if res.status_code == 417:
if res.status_code == 429:
break
else:
self.fail("Rate limting not working")
Expand Down

0 comments on commit 6d3afef

Please sign in to comment.