Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions docs/api-reference/feeds.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
Feeds
=====

PyPI offers two RSS feeds, the `Newest Packages Feed`_ and the `Latest Updates
Feed`_. You can also call its APIs to get more details on project activity.
PyPI offers four RSS feeds, the `Newest Packages Feed`_, the `Latest Updates
Feed`_, the `My Releases Feed`_, and the `My Projects Releases Feed`_. You
can also call its APIs to get more details on project activity.


Newest Packages Feed
Expand All @@ -21,6 +22,24 @@ newly created releases for individual projects on PyPI, including the project
name and description, release version, and a link to the release page.


My Releases Feed
----------------

Available at https://pypi.org/rss/user/USERNAME/my_releases.xml, this feed
provides the latest newly-created releases that ``USERNAME`` has uploaded.
The feed includes the project name, description, release version, and a link
to the project page.


My Projects Releases Feed
-------------------------

Available at https://pypi.org/rss/user/USERNAME/my_project_releases.xml, this
feed provides the latest newly-created releases for projects that ``USERNAME``
owns or maintains. The feed includes the project name, description, release
version, and a link to the project page.


Project and release activity details
------------------------------------

Expand Down
64 changes: 63 additions & 1 deletion tests/unit/rss/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

from warehouse.rss import views as rss

from ...common.db.packaging import ProjectFactory, ReleaseFactory
from ...common.db.accounts import UserFactory
from ...common.db.packaging import ProjectFactory, ReleaseFactory, RoleFactory


def test_rss_updates(db_request):
Expand Down Expand Up @@ -106,3 +107,64 @@ def test_format_author(db_request):
" <[email protected]>"
)
assert rss._format_author(release) == ", ".join(["[email protected]"] * 4)


def test_rss_user_my_releases(db_request):
db_request.find_service = pretend.call_recorder(
lambda *args, **kwargs: pretend.stub(
enabled=False, csp_policy=pretend.stub(), merge=lambda _: None
)
)

db_request.session = pretend.stub()
project1 = ProjectFactory.create()
project2 = ProjectFactory.create()

user = UserFactory.create()

RoleFactory.create(user=user, project=project1)

release1 = ReleaseFactory.create(project=project1)
release1.created = datetime.date(2011, 1, 1)
release1.uploader = user
release2 = ReleaseFactory.create(project=project2)
release2.created = datetime.date(2012, 1, 1)
release2.uploader = user
release3 = ReleaseFactory.create(project=project1)
release3.created = datetime.date(2013, 1, 1)

assert rss.rss_user_my_releases(user, db_request) == {
"user": user,
"latest_releases": [release2, release1],
}
assert db_request.response.content_type == "text/xml"


def test_rss_user_my_project_releases(db_request):
db_request.find_service = pretend.call_recorder(
lambda *args, **kwargs: pretend.stub(
enabled=False, csp_policy=pretend.stub(), merge=lambda _: None
)
)

db_request.session = pretend.stub()
project1 = ProjectFactory.create()
project2 = ProjectFactory.create()

user = UserFactory.create()

RoleFactory.create(user=user, project=project1)

release1 = ReleaseFactory.create(project=project1)
release1.created = datetime.date(2011, 1, 1)
release1.uploader = user
release2 = ReleaseFactory.create(project=project2)
release2.created = datetime.date(2012, 1, 1)
release3 = ReleaseFactory.create(project=project1)
release3.created = datetime.date(2013, 1, 1)

assert rss.rss_user_my_project_releases(user, db_request) == {
"user": user,
"latest_releases": [release3, release1],
}
assert db_request.response.content_type == "text/xml"
14 changes: 14 additions & 0 deletions tests/unit/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,20 @@ def add_policy(name, filename):
pretend.call("ses.hook", "/_/ses-hook/", domain=warehouse),
pretend.call("rss.updates", "/rss/updates.xml", domain=warehouse),
pretend.call("rss.packages", "/rss/packages.xml", domain=warehouse),
pretend.call(
"rss.user_my_releases",
"/rss/user/{username}/my_releases.xml",
factory="warehouse.accounts.models:UserFactory",
traverse="/{username}/",
domain=warehouse,
),
pretend.call(
"rss.user_my_project_releases",
"/rss/user/{username}/my_project_releases.xml",
factory="warehouse.accounts.models:UserFactory",
traverse="/{username}/",
domain=warehouse,
),
pretend.call("legacy.api.simple.index", "/simple/", domain=warehouse),
pretend.call(
"legacy.api.simple.detail",
Expand Down
14 changes: 14 additions & 0 deletions warehouse/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,20 @@ def includeme(config):
# RSS
config.add_route("rss.updates", "/rss/updates.xml", domain=warehouse)
config.add_route("rss.packages", "/rss/packages.xml", domain=warehouse)
config.add_route(
"rss.user_my_releases",
"/rss/user/{username}/my_releases.xml",
factory="warehouse.accounts.models:UserFactory",
traverse="/{username}/",
domain=warehouse,
)
config.add_route(
"rss.user_my_project_releases",
"/rss/user/{username}/my_project_releases.xml",
factory="warehouse.accounts.models:UserFactory",
traverse="/{username}/",
domain=warehouse,
)

# Legacy URLs
config.add_route("legacy.api.simple.index", "/simple/", domain=warehouse)
Expand Down
60 changes: 60 additions & 0 deletions warehouse/rss/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from pyramid.view import view_config
from sqlalchemy.orm import joinedload

from warehouse.accounts.models import User
from warehouse.cache.origin import origin_cache
from warehouse.packaging.models import Project, Release
from warehouse.xml import XML_CSP
Expand Down Expand Up @@ -104,3 +105,62 @@ def rss_packages(request):
]

return {"newest_projects": tuple(zip(newest_projects, project_authors))}


@view_config(
route_name="rss.user_my_releases",
context=User,
renderer="rss/user_my_releases.xml",
decorator=[
origin_cache(
1 * 24 * 60 * 60, # 1 day
stale_while_revalidate=1 * 24 * 60 * 60, # 1 day
stale_if_error=5 * 24 * 60 * 60, # 5 days
)
],
)
def rss_user_my_releases(user, request):
request.response.content_type = "text/xml"

request.find_service(name="csp").merge(XML_CSP)

latest_releases = (
request.db.query(Release)
.filter(Release.uploader_id == user.id)
.options(joinedload(Release.project))
.order_by(Release.created.desc())
.limit(40)
.all()
)

return {"latest_releases": latest_releases, "user": user}


@view_config(
route_name="rss.user_my_project_releases",
context=User,
renderer="rss/user_my_project_releases.xml",
decorator=[
origin_cache(
1 * 24 * 60 * 60, # 1 day
stale_while_revalidate=1 * 24 * 60 * 60, # 1 day
stale_if_error=5 * 24 * 60 * 60, # 5 days
)
],
)
def rss_user_my_project_releases(user, request):
request.response.content_type = "text/xml"

request.find_service(name="csp").merge(XML_CSP)

latest_releases = (
request.db.query(Release)
.join(Project)
.filter(Project.users.any(id=user.id))
.options(joinedload(Release.project))
.order_by(Release.created.desc())
.limit(40)
.all()
)

return {"latest_releases": latest_releases, "user": user}
17 changes: 17 additions & 0 deletions warehouse/templates/accounts/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@

{% block title %}{% trans username=user.username %}Profile of {{ username }}{% endtrans %}{% endblock %}

{% block extra_rss %}
<link rel="alternate" type="application/rss+xml" title="{% trans username=user.username %}RSS: Recent releases from {{ username }}{% endtrans %}" href="{{ request.route_path('rss.user_my_releases', username=user.username) }}">
<link rel="alternate" type="application/rss+xml" title="{% trans username=user.username %}RSS: Recent releases from {{ username }}'s projects{% endtrans %}" href="{{ request.route_path('rss.user_my_project_releases', username=user.username) }}">
{% endblock %}

{% block content %}
<div class="horizontal-section horizontal-section--medium">
<div class="left-layout">
Expand All @@ -40,6 +45,18 @@ <h1 class="author-profile__name">{{ user.name }}</h1>
&nbsp;&nbsp;{% trans start_date=humanize(user.date_joined) %}Joined on {{ start_date }}{% endtrans %}
</p>
{% endif %}
{% if projects %}
<p>
<i class="fa fa-rss" aria-hidden="true"></i>
<span class="sr-only">{% trans %}My Releases RSS Feed{% endtrans %}</span>
&nbsp;&nbsp;<a href="{{ request.route_path('rss.user_my_releases', username=user.username) }}">{% trans %}My Releases{% endtrans %}</a>
</p>
<p>
<i class="fa fa-rss" aria-hidden="true"></i>
<span class="sr-only">{% trans %}My Projects' Releases RSS Feed{% endtrans %}</span>
&nbsp;&nbsp;<a href="{{ request.route_path('rss.user_my_project_releases', username=user.username) }}">{% trans %}My Projects' Releases{% endtrans %}</a>
</p>
{% endif %}
</div>
{% csi request.route_path("includes.profile-actions", username=user.username) %}
{% endcsi %}
Expand Down
1 change: 1 addition & 0 deletions warehouse/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@

<link rel="alternate" type="application/rss+xml" title="{% trans %}RSS: 40 latest updates{% endtrans %}" href="{{ request.route_path('rss.updates') }}">
<link rel="alternate" type="application/rss+xml" title="{% trans %}RSS: 40 newest packages{% endtrans %}" href="{{ request.route_path('rss.packages') }}">
{% block extra_rss %}{% endblock %}
{% if self.canonical_url() %}
<link rel="canonical" href="{% block canonical_url %}{% endblock %}">
{% endif %}
Expand Down
13 changes: 13 additions & 0 deletions warehouse/templates/rss/user_my_project_releases.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends "base.xml" %}
{% block title %}{{ user.username }}'s projects recent releases{% endblock %}
{% block description %}Recent releases from projects {{ user.username }} owns or maintains to the Python Package Index{% endblock %}
{% block items -%}
{% for release in latest_releases %}
<item>
<title>{{ release.project.name }} {{ release.version }}</title>
<link>{{ request.route_url('packaging.release', name=release.project.normalized_name, version=release.version) }}</link>
<description>{{ release.summary }}</description>
<pubDate>{{ release.created|format_rfc822_datetime() }}</pubDate>
</item>
{%- endfor %}
{%- endblock %}
13 changes: 13 additions & 0 deletions warehouse/templates/rss/user_my_releases.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends "base.xml" %}
{% block title %}{{ user.username }}'s recent releases{% endblock %}
{% block description %}Recent releases from {{ user.username }} to the Python Package Index{% endblock %}
{% block items -%}
{% for release in latest_releases %}
<item>
<title>{{ release.project.name }} {{ release.version }}</title>
<link>{{ request.route_url('packaging.release', name=release.project.normalized_name, version=release.version) }}</link>
<description>{{ release.summary }}</description>
<pubDate>{{ release.created|format_rfc822_datetime() }}</pubDate>
</item>
{%- endfor %}
{%- endblock %}