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 25, 2023
1 parent 7c78244 commit 4649496
Show file tree
Hide file tree
Showing 7 changed files with 157 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
Expand Up @@ -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":
Expand All @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions ihatemoney/templates/edit_project.html
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions ihatemoney/templates/list_bills.html
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
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>I Hate Money — {{ g.project.name }}</title>
<description>{% trans project_name=g.project.name %}Latest bills from {{ project_name }}{% 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>
103 changes: 103 additions & 0 deletions ihatemoney/tests/budget_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,17 @@ def test_multiple_join(self):
data = self.client.get("/tartiflette/").data.decode("utf-8")
self.assertEqual(data.count('href="/raclette/"'), 1)

def test_invalid_invite_link(self):
"""Test that a 'feed' token is not valid to join a project"""
self.login("raclette")
self.post_project("raclette")
project = self.get_project("raclette")
invite_link = url_for(
".join_project", project_id="raclette", token=project.generate_token("feed")
)
response = self.client.get(invite_link, follow_redirects=True)
assert "Provided token is invalid" in response.data.decode()

def test_invite_code_invalidation(self):
"""Test that invitation link expire after code change"""
self.login("raclette")
Expand Down Expand Up @@ -1685,6 +1696,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>I Hate Money — raclette</title>
<description>Latest bills from raclette</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
Expand Up @@ -15,6 +15,7 @@

from flask import (
Blueprint,
Response,
abort,
current_app,
flash,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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():
Expand Down

0 comments on commit 4649496

Please sign in to comment.