Repository: pypi/warehouse

Issue:
Display project-urls at the top of the project detail page
We'll want to display the project urls at the top of the project detail page (where homepage/source code/etc are currently). Ideally this will try and select an appropriate icon based on the site.



Discussion:

Resolving patch:
diff --git a/warehouse/config.py b/warehouse/config.py
--- a/warehouse/config.py
+++ b/warehouse/config.py
@@ -239,6 +239,7 @@ def configure(settings=None):
     filters.setdefault("json", "warehouse.filters:tojson")
     filters.setdefault("readme", "warehouse.filters:readme_renderer")
     filters.setdefault("shorten_number", "warehouse.filters:shorten_number")
+    filters.setdefault("urlparse", "warehouse.filters:urlparse")
 
     # We also want to register some global functions for Jinja
     jglobals = config.get_settings().setdefault("jinja2.globals", {})
diff --git a/warehouse/filters.py b/warehouse/filters.py
--- a/warehouse/filters.py
+++ b/warehouse/filters.py
@@ -90,3 +90,7 @@ def shorten_number(value):
 
 def tojson(value):
     return json.dumps(value, sort_keys=True, separators=(",", ":"))
+
+
+def urlparse(value):
+    return urllib.parse.urlparse(value)
diff --git a/warehouse/packaging/models.py b/warehouse/packaging/models.py
--- a/warehouse/packaging/models.py
+++ b/warehouse/packaging/models.py
@@ -12,6 +12,8 @@
 
 import enum
 
+from collections import OrderedDict
+
 from citext import CIText
 from pyramid.security import Allow
 from pyramid.threadlocal import get_current_request
@@ -316,6 +318,22 @@ def __table_args__(cls):  # noqa
         viewonly=True,
     )
 
+    @property
+    def urls(self):
+        _urls = OrderedDict()
+
+        if self.home_page:
+            _urls["Homepage"] = self.home_page
+
+        for urlspec in self.project_urls:
+            name, url = urlspec.split(",", 1)
+            _urls[name] = url
+
+        if self.download_url and "Download" not in _urls:
+            _urls["Download"] = self.download_url
+
+        return _urls
+
     @property
     def has_meta(self):
         return any([self.keywords])


Test patch:
diff --git a/tests/unit/packaging/test_models.py b/tests/unit/packaging/test_models.py
--- a/tests/unit/packaging/test_models.py
+++ b/tests/unit/packaging/test_models.py
@@ -10,12 +10,16 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from collections import OrderedDict
+
 import pretend
 import pytest
 
 from pyramid.security import Allow
 
-from warehouse.packaging.models import ProjectFactory, File
+from warehouse.packaging.models import (
+    ProjectFactory, Dependency, DependencyKind, File,
+)
 
 from ...common.db.packaging import (
     ProjectFactory as DBProjectFactory, ReleaseFactory as DBReleaseFactory,
@@ -107,6 +111,111 @@ def test_has_meta_false(self, db_session):
         release = DBReleaseFactory.create()
         assert not release.has_meta
 
+    @pytest.mark.parametrize(
+        ("home_page", "download_url", "project_urls", "expected"),
+        [
+            (None, None, [], OrderedDict()),
+            (
+                "https://example.com/home/",
+                None,
+                [],
+                OrderedDict([("Homepage", "https://example.com/home/")]),
+            ),
+            (
+                None,
+                "https://example.com/download/",
+                [],
+                OrderedDict([("Download", "https://example.com/download/")]),
+            ),
+            (
+                "https://example.com/home/",
+                "https://example.com/download/",
+                [],
+                OrderedDict([
+                    ("Homepage", "https://example.com/home/"),
+                    ("Download", "https://example.com/download/"),
+                ]),
+            ),
+            (
+                None,
+                None,
+                ["Source Code,https://example.com/source-code/"],
+                OrderedDict([
+                    ("Source Code", "https://example.com/source-code/"),
+                ]),
+            ),
+            (
+                "https://example.com/home/",
+                "https://example.com/download/",
+                ["Source Code,https://example.com/source-code/"],
+                OrderedDict([
+                    ("Homepage", "https://example.com/home/"),
+                    ("Source Code", "https://example.com/source-code/"),
+                    ("Download", "https://example.com/download/"),
+                ]),
+            ),
+            (
+                "https://example.com/home/",
+                "https://example.com/download/",
+                [
+                    "Homepage,https://example.com/home2/",
+                    "Source Code,https://example.com/source-code/",
+                ],
+                OrderedDict([
+                    ("Homepage", "https://example.com/home2/"),
+                    ("Source Code", "https://example.com/source-code/"),
+                    ("Download", "https://example.com/download/"),
+                ]),
+            ),
+            (
+                "https://example.com/home/",
+                "https://example.com/download/",
+                [
+                    "Source Code,https://example.com/source-code/",
+                    "Download,https://example.com/download2/",
+                ],
+                OrderedDict([
+                    ("Homepage", "https://example.com/home/"),
+                    ("Source Code", "https://example.com/source-code/"),
+                    ("Download", "https://example.com/download2/"),
+                ]),
+            ),
+            (
+                "https://example.com/home/",
+                "https://example.com/download/",
+                [
+                    "Homepage,https://example.com/home2/",
+                    "Source Code,https://example.com/source-code/",
+                    "Download,https://example.com/download2/",
+                ],
+                OrderedDict([
+                    ("Homepage", "https://example.com/home2/"),
+                    ("Source Code", "https://example.com/source-code/"),
+                    ("Download", "https://example.com/download2/"),
+                ]),
+            ),
+        ],
+    )
+    def test_urls(self, db_session, home_page, download_url, project_urls,
+                  expected):
+        release = DBReleaseFactory.create(
+            home_page=home_page,
+            download_url=download_url,
+        )
+
+        for urlspec in project_urls:
+            db_session.add(
+                Dependency(
+                    name=release.project.name,
+                    version=release.version,
+                    kind=DependencyKind.project_url.value,
+                    specifier=urlspec,
+                )
+            )
+
+        # TODO: It'd be nice to test for the actual ordering here.
+        assert dict(release.urls) == dict(expected)
+
 
 class TestFile:
 
diff --git a/tests/unit/test_filters.py b/tests/unit/test_filters.py
--- a/tests/unit/test_filters.py
+++ b/tests/unit/test_filters.py
@@ -10,6 +10,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import urllib.parse
+
 import jinja2
 import pretend
 import pytest
@@ -153,3 +155,9 @@ def test_shorten_number(inp, expected):
 )
 def test_tojson(inp, expected):
     assert filters.tojson(inp) == expected
+
+
+def test_urlparse():
+    inp = "https://google.com/foo/bar?a=b"
+    expected = urllib.parse.urlparse(inp)
+    assert filters.urlparse(inp) == expected