diff --git a/tests/unit/rss/test_views.py b/tests/unit/rss/test_views.py index f4f5edb4221e..bb91d0a5cb40 100644 --- a/tests/unit/rss/test_views.py +++ b/tests/unit/rss/test_views.py @@ -35,11 +35,14 @@ def test_rss_updates(db_request): release1.created = datetime.date(2011, 1, 1) release2 = ReleaseFactory.create(project=project2) release2.created = datetime.date(2012, 1, 1) + release2.author_email = "noreply@pypi.org" release3 = ReleaseFactory.create(project=project1) release3.created = datetime.date(2013, 1, 1) assert rss.rss_updates(db_request) == { - "latest_releases": [release3, release2, release1] + "latest_releases": tuple( + zip((release3, release2, release1), (None, "noreply@pypi.org", None)) + ) } assert db_request.response.content_type == "text/xml" @@ -64,5 +67,42 @@ def test_rss_packages(db_request): project3.created = datetime.date(2013, 1, 1) ReleaseFactory.create(project=project3) - assert rss.rss_packages(db_request) == {"newest_projects": [project3, project1]} + assert rss.rss_packages(db_request) == { + "newest_projects": tuple(zip((project3, project1), (None, None))) + } assert db_request.response.content_type == "text/xml" + + +def test_format_author(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() + + project = ProjectFactory.create() + release = ReleaseFactory.create(project=project) + + release.author_email = "noreply@pypi.org" + assert rss._format_author(release) == release.author_email + + release.author_email = "No Reply " + assert rss._format_author(release) == "noreply@pypi.org" + + for invalid in (None, "", "UNKNOWN", "noreply@pypi.org, UNKNOWN"): + release.author_email = invalid + assert rss._format_author(release) is None + + release.author_email = ( + # simple, no spaces + "noreply@pypi.org," + # space after + "noreply@pypi.org ," + # space before, incl realname + " No Reply ," + # two spaces before, angle brackets + " " + ) + assert rss._format_author(release) == ", ".join(["noreply@pypi.org"] * 4) diff --git a/warehouse/rss/views.py b/warehouse/rss/views.py index 46fd24fd2ce0..1341aab032b5 100644 --- a/warehouse/rss/views.py +++ b/warehouse/rss/views.py @@ -10,6 +10,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from email.utils import getaddresses + from pyramid.view import view_config from sqlalchemy.orm import joinedload @@ -18,6 +20,32 @@ from warehouse.xml import XML_CSP +def _format_author(release): + """ + Format release author suitably for inclusion in an RSS feed. + + Release author names and emails are hard to match robustly, mainly + because there may be multiple in both, and especially the names may + contain pretty much anything. So stick with just the emails with some + rudimentary sanity checks. + + Even though the spec says "the email address" and thus assumes a single + author, we let multiple pass, comma separated. + + http://www.rssboard.org/rss-specification#ltauthorgtSubelementOfLtitemgt + """ + author_emails = [] + for _, author_email in getaddresses([release.author_email or ""]): + if "@" not in author_email: + # Require all valid looking + return None + author_emails.append(author_email) + + if not author_emails: + return None + return ", ".join(author_emails) + + @view_config( route_name="rss.updates", renderer="rss/updates.xml", @@ -42,8 +70,9 @@ def rss_updates(request): .limit(40) .all() ) + release_authors = [_format_author(release) for release in latest_releases] - return {"latest_releases": latest_releases} + return {"latest_releases": tuple(zip(latest_releases, release_authors))} @view_config( @@ -70,5 +99,8 @@ def rss_packages(request): .limit(40) .all() ) + project_authors = [ + _format_author(project.releases[0]) for project in newest_projects + ] - return {"newest_projects": newest_projects} + return {"newest_projects": tuple(zip(newest_projects, project_authors))} diff --git a/warehouse/templates/rss/packages.xml b/warehouse/templates/rss/packages.xml index d02e7b365157..6b001adacfb8 100644 --- a/warehouse/templates/rss/packages.xml +++ b/warehouse/templates/rss/packages.xml @@ -2,12 +2,13 @@ {% block title %}PyPI newest packages{% endblock %} {% block description %}Newest packages registered at the Python Package Index{% endblock %} {% block items -%} - {% for project in newest_projects %} + {% for project, author in newest_projects %} {{ project.name }} added to PyPI {{ request.route_url('packaging.project', name=project.normalized_name) }} {{ request.route_url('packaging.project', name=project.normalized_name) }} {{ project.releases[0].summary }} + {% if author %}{{ author }}{% endif %} {{ project.created|format_rfc822_datetime() }} {%- endfor %} diff --git a/warehouse/templates/rss/updates.xml b/warehouse/templates/rss/updates.xml index 204d48f17d60..5dbd982ebc4f 100644 --- a/warehouse/templates/rss/updates.xml +++ b/warehouse/templates/rss/updates.xml @@ -2,11 +2,12 @@ {% block title %}PyPI recent updates{% endblock %} {% block description %}Recent updates to the Python Package Index{% endblock %} {% block items -%} - {% for release in latest_releases %} + {% for release, author in latest_releases %} {{ release.project.name }} {{ release.version }} {{ request.route_url('packaging.release', name=release.project.normalized_name, version=release.version) }} {{ release.summary }} + {% if author %}{{ author }}{% endif %} {{ release.created|format_rfc822_datetime() }} {%- endfor %}