Skip to content

Commit

Permalink
feat: project RSS feed.
Browse files Browse the repository at this point in the history
  • Loading branch information
azmeuk committed Jul 24, 2023
1 parent 7c78244 commit eb2968b
Showing 7 changed files with 146 additions and 4 deletions.
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
include *.rst
recursive-include ihatemoney *.rst *.py *.yaml *.po *.mo *.html *.css *.js *.eot *.svg *.woff *.txt *.png *.webp *.ini *.cfg *.j2 *.jpg *.gif *.ico
recursive-include ihatemoney *.rst *.py *.yaml *.po *.mo *.html *.css *.js *.eot *.svg *.woff *.txt *.png *.webp *.ini *.cfg *.j2 *.jpg *.gif *.ico *.xml
include LICENSE CONTRIBUTORS CHANGELOG.rst
6 changes: 4 additions & 2 deletions ihatemoney/models.py
Original file line number Diff line number Diff line change
@@ -453,7 +453,8 @@ def generate_token(self, token_type="auth"):
"""Generate a timed and serialized JsonWebToken
:param token_type: Either "auth" for authentication (invalidated when project code changed),
or "reset" for password reset (invalidated after expiration)
or "reset" for password reset (invalidated after expiration),
or "feed" for project feeds (invalidated when project code changed)
"""

if token_type == "reset":
@@ -476,7 +477,8 @@ def verify_token(token, token_type="auth", project_id=None, max_age=3600):
:param token: Serialized TimedJsonWebToken
:param token_type: Either "auth" for authentication (invalidated when project code changed),
or "reset" for password reset (invalidated after expiration)
or "reset" for password reset (invalidated after expiration),
or "feed" for project feeds (invalidated when project code changed)
:param project_id: Project ID. Used for token_type "auth" to use the password as serializer
secret key.
:param max_age: Token expiration time (in seconds). Only used with token_type "reset"
4 changes: 4 additions & 0 deletions ihatemoney/templates/edit_project.html
Original file line number Diff line number Diff line change
@@ -36,6 +36,10 @@ <h2>{{ _("Download project's data") }}</h2>
<h5 class="d-flex w-100 justify-content-between">
<span class="mb-1">{{ _('Bill items') }}</span>
<span>
<a href="{{ url_for(".feed", token=g.project.generate_token("feed")) }}" download class="badge badge-secondary">
<i class="icon before-text">{{ static_include("images/globe.svg") | safe }}</i>
RSS
</a>
<a href="{{ url_for('.export_project', file='bills', format='json') }}" download class="badge badge-secondary">
<i class="icon before-text">{{ static_include("images/file-alt.svg") | safe }}</i>
JSON
3 changes: 3 additions & 0 deletions ihatemoney/templates/list_bills.html
Original file line number Diff line number Diff line change
@@ -44,6 +44,9 @@

{% endblock %}

{% block head %}
<link href="{{ url_for(".feed", token=g.project.generate_token("feed")) }}" type="application/rss+xml" rel="alternate" title="{{ g.project.name }}" />
{% endblock %}

{% block sidebar %}
<div class="sidebar_content">
22 changes: 22 additions & 0 deletions ihatemoney/templates/project_feed.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
>
<channel>
<title>{{ g.project.name }}</title>
<description>{% trans %}A simple shared budget manager web application{% endtrans %}</description>
<atom:link href="{{ url_for(".feed", token=g.project.generate_token("feed"), _external=True) }}" rel="self" type="application/rss+xml" />
<link>{{ url_for(".list_bills", _external=True) }}</link>
{% for (weights, bill) in bills.items -%}
<item>
<title>{{ bill.what }} - {{ bill.amount|currency(bill.original_currency) }}</title>
<guid isPermaLink="false">{{ bill.id }}</guid>
<dc:creator>{{ bill.payer }}</dc:creator>
{% if bill.external_link %}<link>{{ bill.external_link }}</link>{% endif -%}
<description>{{ bill.date|dateformat("long") }} - {{ bill.owers|join(', ', 'name') }} : {{ (bill.amount/weights)|currency(bill.original_currency) }}</description>
<pubDate>{{ bill.creation_date.strftime("%a, %d %b %Y %T") }} +0000</pubDate>
</item>
{% endfor -%}
</channel>
</rss>
92 changes: 92 additions & 0 deletions ihatemoney/tests/budget_test.py
Original file line number Diff line number Diff line change
@@ -1685,6 +1685,98 @@ def test_session_projects_migration_to_list(self):
self.assertIsInstance(session["projects"], dict)
self.assertIn("raclette", session["projects"])

def test_rss_feed(self):
self.post_project("raclette")
self.login("raclette")

self.client.post("/raclette/members/add", data={"name": "george"})
self.client.post("/raclette/members/add", data={"name": "peter"})
self.client.post("/raclette/members/add", data={"name": "steven"})

self.client.post(
"/raclette/add",
data={
"date": "2016-12-31",
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"amount": "12",
"original_currency": "EUR",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2016-12-30",
"what": "charcuterie",
"payer": 2,
"payed_for": [1, 2],
"amount": "15",
"original_currency": "EUR",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2016-12-29",
"what": "vin blanc",
"payer": 2,
"payed_for": [1, 2],
"amount": "10",
"original_currency": "EUR",
},
)

project = self.get_project("raclette")
token = project.generate_token("feed")
resp = self.client.get(f"/raclette/feed/{token}.xml")
bills = models.Bill.query.all()
expected_rss_content = f"""<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
>
<channel>
<title>raclette</title>
<description>A simple shared budget manager web application</description>
<atom:link href="http://localhost/raclette/feed/{token}.xml" rel="self" type="application/rss+xml" />
<link>http://localhost/raclette/</link>
<item>
<title>fromage à raclette - €12.00</title>
<guid isPermaLink="false">1</guid>
<dc:creator>george</dc:creator>
<description>December 31, 2016 - george, peter, steven : €4.00</description>
<pubDate>{ bills[0].creation_date.strftime("%a, %d %b %Y %T") } +0000</pubDate>
</item>
<item>
<title>charcuterie - €15.00</title>
<guid isPermaLink="false">2</guid>
<dc:creator>peter</dc:creator>
<description>December 30, 2016 - george, peter : €7.50</description>
<pubDate>{ bills[0].creation_date.strftime("%a, %d %b %Y %T") } +0000</pubDate>
</item>
<item>
<title>vin blanc - €10.00</title>
<guid isPermaLink="false">3</guid>
<dc:creator>peter</dc:creator>
<description>December 29, 2016 - george, peter : €5.00</description>
<pubDate>{ bills[0].creation_date.strftime("%a, %d %b %Y %T") } +0000</pubDate>
</item>
</channel>
</rss>""" # noqa: E501
assert resp.text == expected_rss_content

def test_rss_feed_bad_token(self):
self.post_project("raclette")
self.login("raclette")
project = self.get_project("raclette")
token = project.generate_token("feed")

resp = self.client.get(f"/raclette/feed/{token}.xml")
self.assertEqual(resp.status_code, 200)
resp = self.client.get("/raclette/feed/invalid-token.xml")
self.assertEqual(resp.status_code, 404)


if __name__ == "__main__":
unittest.main()
21 changes: 20 additions & 1 deletion ihatemoney/web.py
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@

from flask import (
Blueprint,
Response,
abort,
current_app,
flash,
@@ -154,7 +155,8 @@ def pull_project(endpoint, values):

is_admin = session.get("is_admin")
is_invitation = endpoint == "main.join_project"
if session.get(project.id) or is_admin or is_invitation:
is_feed = endpoint == "main.feed"
if session.get(project.id) or is_admin or is_invitation or is_feed:
# add project into kwargs and call the original function
g.project = project
else:
@@ -898,6 +900,23 @@ def statistics():
)


@main.route("/<project_id>/feed/<string:token>.xml")
def feed(token):
verified_project_id = Project.verify_token(
token, token_type="feed", project_id=g.project.id
)
if verified_project_id != g.project.id:
abort(404)

weighted_bills = g.project.get_bill_weights_ordered().paginate(
per_page=100, error_out=True
)
return Response(
render_template("project_feed.xml", bills=weighted_bills),
mimetype="application/rss+xml",
)


@main.route("/dashboard")
@requires_admin()
def dashboard():

0 comments on commit eb2968b

Please sign in to comment.