Make URLs more consistent, refactor libraries/releases (#1489)

This commit is contained in:
daveoconnor
2024-11-25 13:08:38 -08:00
committed by GitHub
parent 7e01af3f9c
commit f6a5f4fbcf
35 changed files with 466 additions and 575 deletions

View File

@@ -10,6 +10,7 @@ from django.views.generic import TemplateView
from config.settings import JDOODLE_API_CLIENT_ID, JDOODLE_API_CLIENT_SECRET
from core.calendar import extract_calendar_events, events_by_month, get_calendar
from libraries.constants import LATEST_RELEASE_URL_PATH_STR
from libraries.models import Category, Library
from news.models import Entry
from versions.models import Version
@@ -37,6 +38,7 @@ class HomepageView(TemplateView):
context["num_months"] = len(context["events"])
else:
context["num_months"] = 0
context["LATEST_RELEASE_URL_PATH_STR"] = LATEST_RELEASE_URL_PATH_STR
return context
def get_events(self):

View File

@@ -140,7 +140,7 @@ TEMPLATES = [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"core.context_processors.current_release",
"core.context_processors.current_version",
"core.context_processors.debug",
],
"loaders": [

View File

@@ -1,7 +1,7 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path, re_path
from django.urls import include, path, re_path, register_converter
from django.views.generic import TemplateView
from rest_framework import routers
@@ -32,9 +32,7 @@ from core.views import (
from libraries.api import LibrarySearchView
from libraries.views import (
LibraryDetail,
LibraryList,
LibraryListByCategory,
LibraryListMini,
LibraryListDispatcher,
)
from news.feeds import AtomNewsFeed, RSSNewsFeed
from news.views import (
@@ -71,6 +69,7 @@ from users.views import (
DeleteImmediatelyView,
)
from versions.api import ImportVersionsView, VersionViewSet
from versions.converters import BoostVersionSlugConverter
from versions.feeds import AtomVersionFeed, RSSVersionFeed
from versions.views import (
InProgressReleaseNotesView,
@@ -79,13 +78,14 @@ from versions.views import (
VersionDetail,
)
register_converter(BoostVersionSlugConverter, "boostversionslug")
router = routers.SimpleRouter()
router.register(r"users", UserViewSet, basename="users")
router.register(r"versions", VersionViewSet, basename="versions")
router.register(r"libraries", LibrarySearchView, basename="libraries")
urlpatterns = (
[
path("", HomepageView.as_view(), name="home"),
@@ -157,7 +157,11 @@ urlpatterns = (
InProgressReleaseNotesView.as_view(),
name="release-in-progress",
),
path("releases/<slug:slug>/", VersionDetail.as_view(), name="release-detail"),
path(
"releases/<boostversionslug:version_slug>/",
VersionDetail.as_view(),
name="release-detail",
),
path(
"donate/",
TemplateView.as_view(template_name="donate/donate.html"),
@@ -168,27 +172,25 @@ urlpatterns = (
TemplateView.as_view(template_name="style_guide.html"),
name="style-guide",
),
path("libraries/", LibraryListDispatcher.as_view(), name="libraries"),
path(
"libraries/by-category/",
LibraryListByCategory.as_view(),
name="libraries-by-category",
),
path("libraries/", LibraryList.as_view(), name="libraries"),
path("libraries/mini/", LibraryListMini.as_view(), name="libraries-mini"),
path("libraries/grid/", LibraryList.as_view(), name="libraries-grid"),
path(
"libraries/<slug:slug>/<slug:version_slug>/",
LibraryDetail.as_view(),
name="library-detail-by-version",
"libraries/<boostversionslug:version_slug>/<str:library_view_str>/",
LibraryListDispatcher.as_view(),
name="libraries-list",
),
path(
"libraries/<slug:slug>/",
"libraries/<boostversionslug:version_slug>/<str:library_view_str>/<slug:category_slug>/",
LibraryListDispatcher.as_view(),
name="libraries-list",
),
path(
"library/<boostversionslug:version_slug>/<slug:library_slug>/",
LibraryDetail.as_view(),
name="library-detail",
),
# Redirect for '/libs/' legacy boost.org urls.
re_path(
r"^libs/(?P<slug>[-\w]+)/?$",
r"^libs/(?P<library_slug>[-\w]+)/?$",
LibraryDetail.as_view(redirect_to_docs=True),
name="library-docs-redirect",
),

View File

@@ -3,10 +3,9 @@ from django.conf import settings
from versions.models import Version
def current_release(request):
def current_version(request):
"""Custom context processor that adds the current release to the context"""
current_release = Version.objects.most_recent()
return {"current_release": current_release}
return {"current_version": Version.objects.most_recent()}
def debug(request):

View File

@@ -1,12 +1,12 @@
from core.context_processors import current_release
from core.context_processors import current_version
def test_current_release_context(
def test_current_version_context(
version, beta_version, inactive_version, old_version, rf
):
"""Test the current_release context processor. Making the other versions
"""Test the current_version context processor. Making the other versions
ensures that the most_recent() method returns the correct version."""
request = rf.get("/")
context = current_release(request)
assert "current_release" in context
assert context["current_release"] == version
context = current_version(request)
assert "current_version" in context
assert context["current_version"] == version

View File

@@ -352,8 +352,9 @@ README_MISSING = (
"consider contributing one."
)
DEFAULT_LIBRARIES_LANDING_VIEW = "libraries-grid"
DEFAULT_LIBRARIES_LANDING_VIEW = "grid"
SELECTED_BOOST_VERSION_COOKIE_NAME = "boost_version"
SELECTED_LIBRARY_VIEW_COOKIE_NAME = "library_view"
# change this to switch from /libraries/align/release/ to /libraries/align/latest/
LATEST_RELEASE_URL_PATH_STR = "release"
LATEST_RELEASE_URL_PATH_STR = "latest"
LEGACY_LATEST_RELEASE_URL_PATH_STR = "release"
VERSION_SLUG_PREFIX = "boost-"

View File

@@ -1,10 +1,7 @@
from urllib.parse import urlencode
import structlog
from django.urls import reverse
from libraries.constants import LATEST_RELEASE_URL_PATH_STR
from libraries.utils import determine_selected_boost_version
from versions.models import Version
logger = structlog.get_logger()
@@ -15,53 +12,37 @@ class VersionAlertMixin:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
current_release = Version.objects.most_recent()
url_name = self.request.resolver_match.url_name
url_names_version_slug_override = {
"library-detail": "version_slug",
"library-detail-by-version": "version_slug",
}
version_slug_name = url_names_version_slug_override.get(url_name, "slug")
version_slug = self.kwargs.get(
version_slug_name,
self.request.GET.get("version"),
if url_name in {"libraries", "releases-most-recent"}:
return context
current_version_kwargs = self.kwargs.copy()
current_version_kwargs.update({"version_slug": LATEST_RELEASE_URL_PATH_STR})
context["version_alert_url"] = reverse(url_name, kwargs=current_version_kwargs)
context["version_alert"] = (
self.kwargs.get("version_slug") != LATEST_RELEASE_URL_PATH_STR
)
selected_boost_version = determine_selected_boost_version(
version_slug, self.request
)
if not selected_boost_version:
selected_boost_version = LATEST_RELEASE_URL_PATH_STR
version_slug = LATEST_RELEASE_URL_PATH_STR
try:
selected_version = Version.objects.get(slug=selected_boost_version)
except Version.DoesNotExist:
selected_version = current_release
def generate_reverse(url_name):
if url_name in {
"libraries-mini",
"libraries-by-category",
"libraries-grid",
}:
url = reverse(url_name)
params = {"version": LATEST_RELEASE_URL_PATH_STR}
return f"{url}?{urlencode(params)}"
elif url_name == "library-detail-by-version":
library_slug = self.kwargs.get(
"slug"
) # only really accurately set on library detail page
return reverse("library-detail", args=[library_slug])
elif url_name == "release-detail":
return reverse(url_name, args=[LATEST_RELEASE_URL_PATH_STR])
# 'version_str' is representative of what the user has chosen, while 'version'
# is the actual version instance that will be used in the template. We use the
# value of LATEST_RELEASE_URL_PATH_STR as the default in order to normalize
# behavior
context["version"] = selected_version
context["version_str"] = version_slug
context["LATEST_RELEASE_URL_PATH_STR"] = LATEST_RELEASE_URL_PATH_STR
context["current_release"] = current_release
context["version_alert"] = context["version_str"] != LATEST_RELEASE_URL_PATH_STR
context["version_alert_url"] = generate_reverse(url_name)
return context
class BoostVersionMixin:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# todo: replace get_current_library_version on LibraryDetail with this +
# prefetch_related
context.update(
{
"version_str": self.kwargs.get("version_slug"),
"LATEST_RELEASE_URL_PATH_STR": LATEST_RELEASE_URL_PATH_STR,
}
)
if not context.get("current_version"):
context["current_version"] = Version.objects.most_recent()
if context["version_str"] == LATEST_RELEASE_URL_PATH_STR:
context["selected_version"] = context["current_version"]
elif context["version_str"]:
context["selected_version"] = Version.objects.get(
slug=context["version_str"]
)
return context

View File

@@ -13,6 +13,7 @@ def library(db):
return baker.make(
"libraries.Library",
name="multi_array",
slug="multi_array",
description=(
"Boost.MultiArray provides a generic N-dimensional array concept "
"definition and common implementations of that interface."

View File

@@ -3,10 +3,10 @@ import pytest
from django.test import RequestFactory
from model_bakery import baker
from libraries.mixins import VersionAlertMixin
from libraries.views import LibraryList
from libraries.views import LibraryListBase
class MockView(LibraryList, VersionAlertMixin):
class MockView(LibraryListBase, VersionAlertMixin):
pass

View File

@@ -9,9 +9,11 @@ from ..models import Library
from versions.models import Version
def test_library_list(library_version, tp, url_name="libraries"):
def test_library_list(library_version, tp, url_name="libraries", request_kwargs=None):
"""GET /libraries/"""
# Create a version with a library
if not request_kwargs:
request_kwargs = {}
last_year = library_version.version.release_date - datetime.timedelta(days=365)
v2 = baker.make(
"versions.Version", name="boost-1.78.0", release_date=last_year, beta=False
@@ -26,105 +28,114 @@ def test_library_list(library_version, tp, url_name="libraries"):
v_no_libraries = baker.make(
"versions.Version", name="boost-1.0.0", release_date=last_year, beta=False
)
url = tp.reverse(url_name, **request_kwargs)
res = tp.get(url, **request_kwargs)
res = tp.get(url_name)
if url_name == "libraries":
tp.response_302(res)
return
tp.response_200(res)
assert "library_list" in res.context
assert library_version.library in res.context["library_list"]
assert lib2 not in res.context["library_list"]
assert "object_list" in res.context
assert library_version in res.context["object_list"]
assert lib2 not in res.context["object_list"]
assert v_no_libraries not in res.context["versions"]
def test_library_root_redirect_to_grid(tp):
"""GET /"""
"""GET /libraries/"""
res = tp.get("libraries")
tp.response_302(res)
assert res.url == "/libraries/grid/"
assert res.url == "/libraries/latest/grid/"
def test_library_list_no_data(tp):
"""GET /libraries/"""
"""GET /libraries/latest/grid/"""
Library.objects.all().delete()
Version.objects.all().delete()
res = tp.get("libraries-grid")
res = tp.get("/libraries/latest/grid/")
tp.response_200(res)
def test_library_list_mini(library_version, tp):
"""GET /libraries/mini/"""
test_library_list(library_version, tp, url_name="libraries-mini")
def test_library_list_list(library_version, tp):
"""GET /libraries/latest/list"""
test_library_list(
library_version,
tp,
url_name="libraries-list",
request_kwargs={"version_slug": "latest", "library_view_str": "list"},
)
def test_library_list_mini_no_data(tp):
def test_library_list_list_no_data(tp):
"""GET /libraries/"""
Library.objects.all().delete()
Version.objects.all().delete()
res = tp.get("libraries-mini")
url = tp.reverse("libraries-list", "latest", "list")
res = tp.get(url)
tp.response_200(res)
def test_library_list_no_pagination(library_version, tp):
"""Library list is not paginated."""
libs = [
lib_versions = [
baker.make(
"libraries.LibraryVersion",
library=baker.make("libraries.Library", name=f"lib-{i}"),
version=library_version.version,
).library
)
for i in range(30)
] + [library_version.library]
res = tp.get("libraries-grid")
] + [library_version]
url = tp.reverse("libraries-list", "latest", "grid")
res = tp.get(url)
tp.response_200(res)
library_list = res.context.get("library_list")
assert library_list is not None
assert len(library_list) == len(libs)
assert all(library in library_list for library in libs)
library_version_list = res.context.get("object_list")
assert library_version_list is not None
assert len(library_version_list) == len(lib_versions)
assert all(lv in library_version_list for lv in lib_versions)
page_obj = res.context.get("page_obj")
assert getattr(page_obj, "paginator", None) is None
def test_library_list_select_category(library_version, category, tp):
"""GET /libraries/?category={{ slug }} loads filtered results"""
"""GET /libraries/latest/grid/{category_slug}/ loads filtered results"""
library_version.library.categories.add(category)
# Create a new library version that is not in the selected category
new_lib = baker.make("libraries.Library", name="New")
new_lib_version = baker.make(
"libraries.LibraryVersion", version=library_version.version, library=new_lib
)
res = tp.get(f"/libraries/grid/?category={category.slug}")
res = tp.get(f"/libraries/latest/grid/{category.slug}/")
tp.response_200(res)
assert library_version.library in res.context["library_list"]
assert new_lib_version.library not in res.context["library_list"]
assert library_version in res.context["object_list"]
assert new_lib_version not in res.context["object_list"]
@pytest.mark.skip(
reason="This test is failing due to the way the library list is being filtered"
)
def test_library_list_select_version(library_version, tp):
"""GET /libraries/?version={{ slug }} loads filtered results"""
"""GET /libraries/{version_slug}/list/ loads filtered results"""
new_version = baker.make("versions.Version", name="New")
new_lib = baker.make("libraries.Library", name="New")
# Create a new library version that is not in the selected version
new_lib_version = baker.make(
"libraries.LibraryVersion", version=new_version, library=new_lib
)
res = tp.get(f"/libraries/?version={library_version.version.slug}")
res = tp.get(f"/libraries/{library_version.version.slug}/list/")
tp.response_200(res)
assert library_version.library in res.context["library_list"]
assert new_lib_version.library not in res.context["library_list"]
assert library_version.library in res.context["object_list"]
assert new_lib_version.library not in res.context["object_list"]
def test_library_list_by_category(
library_version, category, tp, url="libraries-by-category"
):
"""GET /libraries/by-category/"""
def test_library_list_by_category(library_version, category, tp, url="libraries-list"):
"""GET /libraries/latest/categorized/"""
# this first part of the test is weird, maybe a change in functionality happened?
# the categorized view shows all categories - the category slug is ignored - so
# all categories show
reverse_url = tp.reverse(url, "latest", "categorized", category.slug)
library_version.library.categories.add(category)
res = tp.get(url)
res = tp.get(reverse_url)
tp.response_200(res)
assert "library_versions_by_category" in res.context
assert "category" in res.context["library_versions_by_category"][0]
@@ -136,8 +147,10 @@ def test_library_list_by_category(
new_version = baker.make("versions.Version", name="New")
new_lib = baker.make("libraries.Library", name="New", categories=[new_category])
baker.make("libraries.LibraryVersion", version=new_version, library=new_lib)
res = tp.get(f"/libraries/by-category/?version={library_version.version.slug}")
url = tp.reverse("libraries-list", library_version.version.slug, "categorized")
res = tp.get(url)
tp.response_200(res)
tp.assertContext("version_slug", library_version.version.slug)
assert existing_category in [
x["category"] for x in res.context["library_versions_by_category"]
]
@@ -147,29 +160,29 @@ def test_library_list_by_category(
def test_library_detail(library_version, tp):
"""GET /libraries/{slug}/"""
"""GET /library/latest/{library_slug}/"""
library = library_version.library
url = tp.reverse("library-detail", library.slug)
url = tp.reverse("library-detail", "latest", library.slug)
response = tp.get(url)
tp.response_200(response)
def test_library_detail_404(library, tp):
"""GET /libraries/{slug}/"""
"""GET /libraries/latest/{bad_library_slug}/"""
# 404 due to bad slug
url = tp.reverse("library-detail", "bananas")
url = tp.reverse("library-detail", "latest", "bananas")
response = tp.get(url)
tp.response_404(response)
# 404 due to no existing version
url = tp.reverse("library-detail", library.slug)
url = tp.reverse("library-detail", "latest", library.slug)
response = tp.get(url)
tp.response_404(response)
def test_library_docs_redirect(tp, library, library_version):
"""
GET /libs/{slug}/
GET /libs/{library_slug}/
Test that redirection occurs when the library has a documentation URL
"""
url = tp.reverse("library-docs-redirect", library.slug)
@@ -181,7 +194,7 @@ def test_library_docs_redirect(tp, library, library_version):
def test_library_detail_context_get_commit_data_(tp, library_version):
"""
GET /libraries/{slug}/
GET /library/latest/{library_slug}/
Test that the commit_data_by_release var appears as expected
"""
library = library_version.library
@@ -197,14 +210,14 @@ def test_library_detail_context_get_commit_data_(tp, library_version):
for lv in [lv_a, lv_b, lv_c]:
baker.make("libraries.Commit", library_version=lv)
url = tp.reverse("library-detail", library.slug)
url = tp.reverse("library-detail", "latest", library.slug)
response = tp.get_check_200(url)
assert "commit_data_by_release" in response.context
def test_library_detail_context_get_maintainers(tp, user, library_version):
"""
GET /libraries/{slug}/
GET /libraries/latest/{library_slug}/
Test that the maintainers var appears as expected
"""
library_version.maintainers.add(user)
@@ -214,7 +227,7 @@ def test_library_detail_context_get_maintainers(tp, user, library_version):
baker.make("libraries.PullRequest", library=library, is_open=True)
baker.make("libraries.PullRequest", library=library, is_open=False)
baker.make("libraries.PullRequest", library=lib2, is_open=True)
url = tp.reverse("library-detail", library.slug)
url = tp.reverse("library-detail", "latest", library.slug)
response = tp.get(url)
tp.response_200(response)
assert "maintainers" in response.context
@@ -226,52 +239,49 @@ def test_library_detail_context_get_documentation_url_no_docs_link(
tp, user, library_version
):
"""
GET /libraries/{slug}/
Test that the maintainers var appears as expected
GET /library/{version_slug}/{library_slug}/
"""
library_version.documentation_url = None
library_version.save()
library = library_version.library
url = tp.reverse("library-detail", library.slug)
url = tp.reverse("library-detail", library_version.version.slug, library.slug)
response = tp.get(url)
tp.response_200(response)
assert "documentation_url" in response.context
assert response.context["documentation_url"] == "/doc/libs/release"
assert response.context["documentation_url"] == "/doc/libs/1_79_0"
def test_library_detail_context_get_documentation_url_missing_docs_bool(
tp, user, library_version
):
"""
GET /libraries/{slug}/
Test that the maintainers var appears as expected
GET /library/{version_slug}/{library_slug}/
"""
library_version.documentation_url = None
library_version.missing_docs = True
library_version.save()
library = library_version.library
url = tp.reverse("library-detail", library.slug)
url = tp.reverse("library-detail", library_version.version.slug, library.slug)
response = tp.get(url)
tp.response_200(response)
assert "documentation_url" in response.context
assert response.context["documentation_url"] == "/doc/libs/release"
assert response.context["documentation_url"] == "/doc/libs/1_79_0"
def test_library_detail_context_get_documentation_url_docs_present(
tp, user, library_version
):
"""
GET /libraries/{slug}/
Test that the maintainers var appears as expected
GET /libraries/{version_slug}/{library_slug}/
"""
library_version.documentation_url = "https://example.com"
library_version.missing_docs = False
library_version.save()
library = library_version.library
url = tp.reverse("library-detail", library.slug)
url = tp.reverse("library-detail", library_version.version.slug, library.slug)
response = tp.get(url)
tp.response_200(response)
assert "documentation_url" in response.context
@@ -279,44 +289,46 @@ def test_library_detail_context_get_documentation_url_docs_present(
def test_libraries_by_version_detail(tp, library_version):
"""GET /libraries/{slug}/{version_slug}/"""
"""GET /libraries/{version_slug}/{library_slug}/"""
res = tp.get(
"library-detail-by-version",
library_version.library.slug,
"library-detail",
library_version.version.slug,
library_version.library.slug,
)
tp.response_200(res)
assert "version" in res.context
assert "current_version" in res.context
assert "selected_version" in res.context
assert res.context["selected_version"] == library_version.version
def test_libraries_by_version_detail_no_library_found(tp, library_version):
"""GET /libraries/{slug}/{version_slug}/"""
"""GET /library/{version_slug}/{bad_library_slug}/"""
res = tp.get(
"library-detail-by-version",
"coffee",
"library-detail",
library_version.version.slug,
"coffee",
)
tp.response_404(res)
def test_libraries_by_version_detail_no_version_found(tp, library_version):
"""GET /libraries/{slug}/{version_slug}/"""
"""GET /library/{version_slug}/{bad_library_slug}/"""
res = tp.get(
"library-detail-by-version",
library_version.library.slug,
"library-detail",
000000,
library_version.library.slug,
)
tp.response_404(res)
def test_library_detail_context_missing_readme(tp, user, library_version):
"""
GET /libraries/{slug}/
GET /library/latest/{library_slug}/
Test that the missing readme message appears as expected
"""
library = library_version.library
url = tp.reverse("library-detail", library.slug)
url = tp.reverse("library-detail", "latest", library.slug)
response = tp.get(url)

View File

@@ -6,12 +6,9 @@ import structlog
import tempfile
from datetime import datetime
from dateutil.relativedelta import relativedelta
from urllib.parse import urlencode
from dateutil.parser import ParserError, parse
from django.utils.text import slugify
from django.urls import reverse
from django.shortcuts import redirect
from libraries.constants import (
DEFAULT_LIBRARIES_LANDING_VIEW,
@@ -91,16 +88,6 @@ def write_content_to_tempfile(content):
return temp_file
def redirect_to_view_with_params(view_name, params, query_params):
"""Redirect to a view with parameters and query parameters."""
base_url = reverse(view_name, kwargs=params)
query_string = urlencode(query_params)
url = base_url
if query_string:
url = "{}?{}".format(base_url, query_string)
return redirect(url)
def get_version_from_url(request):
return request.GET.get("version")
@@ -110,7 +97,7 @@ def get_version_from_cookie(request):
def get_view_from_url(request):
return request.GET.get("view")
return request.resolver_match.kwargs.get("library_view_str")
def get_view_from_cookie(request):
@@ -118,6 +105,9 @@ def get_view_from_cookie(request):
def set_view_in_cookie(response, view):
allowed_views = {"grid", "list", "categorized"}
if view not in allowed_views:
return
response.set_cookie(SELECTED_LIBRARY_VIEW_COOKIE_NAME, view)
@@ -146,34 +136,14 @@ def get_prioritized_library_view(request):
return url_view or cookie_view or DEFAULT_LIBRARIES_LANDING_VIEW
def build_view_query_params_from_request(request):
query_params = {}
version = get_prioritized_version(request)
category = get_category(request)
if version and version != LATEST_RELEASE_URL_PATH_STR:
query_params["version"] = version
if category:
query_params["category"] = category
return query_params
def get_category(request):
return request.GET.get("category", "")
def build_route_name_for_view(view):
return f"libraries-{view}"
def determine_view_from_library_request(request):
split_path_info = request.path_info.split("/")
return None if split_path_info[-2] == "libraries" else split_path_info[-2]
def determine_selected_boost_version(request_value, request):
valid_versions = Version.objects.version_dropdown_strict()
version_slug = request_value or get_version_from_cookie(request)
if version_slug in [v.slug for v in valid_versions]:
if version_slug in [v.slug for v in valid_versions] + [LATEST_RELEASE_URL_PATH_STR]:
return version_slug
else:
logger.warning(f"Invalid version slug in cookies: {version_slug}")
@@ -192,9 +162,9 @@ def set_selected_boost_version(version_slug: str, response) -> None:
def library_doc_latest_transform(url):
p = re.compile(r"(/doc/libs/)[a-zA-Z0-9_]+([//\S]*)$")
p = re.compile(r"^(/doc/libs/)[0-9_]+(/\S+)$")
if p.match(url):
url = p.sub(r"\1release\2", url)
url = p.sub(rf"\1{LATEST_RELEASE_URL_PATH_STR}\2", url)
return url
@@ -202,18 +172,15 @@ def get_documentation_url(library_version, latest):
"""Get the documentation URL for the current library."""
def find_documentation_url(library_version):
version = library_version.version
docs_url = version.documentation_url
# If we know the library-version docs are missing, return the version docs
if library_version.missing_docs:
return docs_url
return library_version.version.documentation_url
# If we have the library-version docs and they are valid, return those
elif library_version.documentation_url:
return library_version.documentation_url
# If we wind up here, return the version docs
else:
return docs_url
return library_version.version.documentation_url
# Get the URL for the version.
url = find_documentation_url(library_version)

View File

@@ -1,25 +1,23 @@
import datetime
from types import SimpleNamespace
import structlog
from django.contrib import messages
from django.db.models import F, Count, Exists, OuterRef, Prefetch
from django.db.models.functions import Lower
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import DetailView, ListView
from django.views.generic.edit import FormMixin
from django import urls
from django.http import HttpResponseRedirect
from core.githubhelper import GithubAPIClient
from versions.models import Version
from .constants import README_MISSING
from .forms import VersionSelectionForm
from .mixins import VersionAlertMixin
from .mixins import VersionAlertMixin, BoostVersionMixin
from .models import (
Category,
Commit,
@@ -29,118 +27,87 @@ from .models import (
LibraryVersion,
)
from .utils import (
redirect_to_view_with_params,
get_view_from_cookie,
set_view_in_cookie,
get_prioritized_library_view,
build_view_query_params_from_request,
build_route_name_for_view,
determine_view_from_library_request,
determine_selected_boost_version,
set_selected_boost_version,
get_documentation_url,
)
from .constants import LATEST_RELEASE_URL_PATH_STR
logger = structlog.get_logger()
class LibraryList(VersionAlertMixin, ListView):
"""List all of our libraries for a specific Boost version, or default
to the current version."""
queryset = (
(
Library.objects.prefetch_related("authors", "categories")
.all()
.order_by("name")
)
.defer("data")
.distinct()
)
template_name = "libraries/list.html"
def get_queryset(self):
queryset = super().get_queryset()
params = self.request.GET.copy()
# If the user has selected a version, fetch it from the cookies.
selected_boost_version = (
class LibraryListDispatcher(View):
def dispatch(self, request, *args, **kwargs):
view_str = self.kwargs.get("library_view_str")
if view_str == "list":
view = LibraryVertical.as_view()
elif view_str == "categorized":
view = LibraryCategorized.as_view()
else:
# covers both /libraries and /libraries/.../grid[/...]
view = LibraryListBase.as_view()
version_str = (
determine_selected_boost_version(
self.request.GET.get("version"), self.request
self.kwargs.get("version_slug"), self.request
)
or LATEST_RELEASE_URL_PATH_STR
)
# default to the most recent version
if selected_boost_version == LATEST_RELEASE_URL_PATH_STR:
# If no version is specified, show the most recent version.
if not self.kwargs.get("version_slug"):
self.kwargs["version_slug"] = version_str
return view(request, *args, **self.kwargs) # , *args, **kwargs)
class LibraryListBase(BoostVersionMixin, VersionAlertMixin, ListView):
"""Based on LibraryVersion, list all of our libraries in grid format for a specific
Boost version, or default to the current version."""
queryset = LibraryVersion.objects.prefetch_related(
"authors", "library", "library__categories"
).defer("data")
ordering = "library__name"
template_name = "libraries/grid_list.html"
def get_queryset(self):
queryset = super().get_queryset()
version_slug = determine_selected_boost_version(
self.kwargs.get("version_slug"), self.request
)
if version_slug == LATEST_RELEASE_URL_PATH_STR:
version = Version.objects.most_recent()
if version:
selected_boost_version = version.slug
else:
# Add a message that no data has been imported
if not version:
messages.add_message(
self.request,
messages.WARNING,
"No data has been imported yet. Please check back later.",
)
return Library.objects.none()
version_slug = version.slug
queryset = queryset.filter(
library_version__version__slug=selected_boost_version
)
version_filter_args = {"version__slug": version_slug}
# avoid attempting to look up libraries with blank categories
if params.get("category"):
queryset = queryset.filter(categories__slug=params.get("category"))
no_category_filtering_views = ["categorized"]
if (
self.kwargs.get("category_slug")
and self.kwargs.get("library_view_str") not in no_category_filtering_views
):
version_filter_args["library__categories__slug"] = self.kwargs.get(
"category_slug"
)
return queryset
return queryset.filter(**version_filter_args)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Handle the case where data hasn't been imported yet
version = Version.objects.most_recent()
version_str = determine_selected_boost_version(
self.request.GET.get("version"), self.request
context = super().get_context_data(**self.kwargs)
context["categories"] = self.get_categories(context["selected_version"])
context["versions"] = self.get_versions(
current_version=context["current_version"]
)
if not version_str:
version_str = LATEST_RELEASE_URL_PATH_STR
if not version:
context.update(
{
"category": None,
"version": None,
"version_str": version_str,
"categories": Category.objects.none(),
"versions": Version.objects.none(),
"library_version_list": LibraryVersion.objects.none(),
}
)
return context
if self.request.GET.get("category"):
# todo: add tests for sort order
if self.kwargs.get("category_slug"):
context["category"] = Category.objects.get(
slug=self.request.GET["category"]
slug=self.kwargs.get("category_slug")
)
context["categories"] = self.get_categories(context["version"])
context["versions"] = self.get_versions()
context["version_str"] = version_str
# todo: add tests for sort order, consider refactor to queryset use
library_versions_qs = (
LibraryVersion.objects.filter(
version__slug=version_str
if version_str != LATEST_RELEASE_URL_PATH_STR
else version.slug
)
.prefetch_related("authors", "library", "library__categories")
.order_by("library__name")
)
if self.request.GET.get("category"):
library_versions_qs = library_versions_qs.filter(
library__categories__slug=self.request.GET.get("category")
)
context["library_version_list"] = library_versions_qs
context["url_params"] = build_view_query_params_from_request(self.request)
return context
@@ -151,7 +118,7 @@ class LibraryList(VersionAlertMixin, ListView):
.order_by("name")
)
def get_versions(self):
def get_versions(self, current_version):
"""
Return a queryset of all versions to display in the version dropdown.
"""
@@ -165,70 +132,82 @@ class LibraryList(VersionAlertMixin, ListView):
# Filter out versions with no libraries
versions = versions.filter(library_count__gt=0)
most_recent_version = Version.objects.most_recent()
# Confirm the most recent v is in the queryset, even if it has no libraries
if most_recent_version not in versions:
versions = versions | Version.objects.filter(pk=most_recent_version.pk)
if current_version and current_version not in versions:
versions = versions | Version.objects.filter(pk=current_version.pk)
# Manually exclude the master and develop branches.
versions = versions.exclude(name__in=["develop", "master", "head"])
# todo: confirm is redundant with version_dropdown()'s matching exclude
# versions = versions.exclude(name__in=["develop", "master", "head"])
versions.prefetch_related("library_version")
return versions
def dispatch(self, request, *args, **kwargs):
"""Set the selected version in the cookies."""
response = super().dispatch(request, *args, **kwargs)
query_params = build_view_query_params_from_request(request)
set_selected_boost_version(
query_params.get("version", LATEST_RELEASE_URL_PATH_STR), response
)
# The following conditional practically only applies on "/libraries/", at
# which point the redirection will be determined by prioritised view
view = determine_view_from_library_request(request)
if not view:
view = get_prioritized_library_view(request)
set_view_in_cookie(response, build_route_name_for_view(view))
return redirect_to_view_with_params(view, kwargs, query_params)
set_selected_boost_version(self.kwargs.get("version_slug"), response)
view = get_prioritized_library_view(request)
if request.resolver_match.view_name == "libraries":
# todo: remove the following migration block some time after March 1st 2025
def update_deprecated_cookie_view(cookie_view, response):
deprecated_views = {
"libraries-mini": "list",
"libraries-grid": "grid",
"libraries-by-category": "categorized",
}
if cookie_view in deprecated_views:
cookie_view = deprecated_views[cookie_view]
set_view_in_cookie(response, cookie_view)
return cookie_view
view = update_deprecated_cookie_view(view, response)
# todo: end of migration block
# set the cookie in case it has changed
set_view_in_cookie(response, view)
redirect_args = {
"version_slug": self.kwargs.get("version_slug"),
"library_view_str": view,
}
if self.kwargs.get("category_slug"):
redirect_args["category_slug"] = self.kwargs.get("category_slug")
return redirect("libraries-list", **redirect_args)
if view != get_view_from_cookie(request):
set_view_in_cookie(response, build_route_name_for_view(view))
set_view_in_cookie(response, view)
return response
class LibraryListMini(LibraryList):
class LibraryVertical(LibraryListBase):
"""Flat list version of LibraryList"""
template_name = "libraries/flat_list.html"
template_name = "libraries/vertical_list.html"
class LibraryListByCategory(LibraryList):
class LibraryCategorized(LibraryListBase):
"""List all Boost libraries sorted by Category."""
template_name = "libraries/category_list.html"
template_name = "libraries/categorized_list.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["library_versions_by_category"] = self.get_results_by_category(
version=context.get("version")
version=context.get("selected_version")
)
return context
def get_results_by_category(self, version: Version | None):
# Define filter kwargs based on whether version is provided
version_filter = {"version": version} if version else {}
category_filter = (
{"libraries__library_version__version": version} if version else {}
)
library_versions_qs = LibraryVersion.objects.filter(**version_filter)
libraries_prefetch = Prefetch(
"libraries",
queryset=Library.objects.order_by("name").prefetch_related(
Prefetch(
"library_version",
queryset=library_versions_qs,
queryset=self.get_queryset(),
to_attr="prefetched_library_versions",
)
),
@@ -258,7 +237,7 @@ class LibraryListByCategory(LibraryList):
@method_decorator(csrf_exempt, name="dispatch")
class LibraryDetail(FormMixin, VersionAlertMixin, DetailView):
class LibraryDetail(FormMixin, VersionAlertMixin, BoostVersionMixin, DetailView):
"""Display a single Library in insolation"""
form_class = VersionSelectionForm
@@ -274,26 +253,24 @@ class LibraryDetail(FormMixin, VersionAlertMixin, DetailView):
Version.objects.active()
.filter(library_version__library=self.object)
.distinct()
.exclude(name__in=["develop", "master", "head"])
.exclude(beta=True)
.order_by("-release_date")
)
# Manually exclude feature branches from the version dropdown.
context["versions"] = context["versions"].exclude(
name__in=["develop", "master", "head"]
)
# Manually exclude beta releases from the version dropdown.
context["versions"] = context["versions"].exclude(beta=True)
context["LATEST_RELEASE_URL_PATH_NAME"] = LATEST_RELEASE_URL_PATH_STR
# Get general data and version-sensitive data
library_version = LibraryVersion.objects.get(
library=self.get_object(), version=context["version"]
library=self.get_object(), version=context["selected_version"]
)
context["library_version"] = library_version
context["documentation_url"] = get_documentation_url(
library_version, context["version_str"] == LATEST_RELEASE_URL_PATH_STR
)
context["github_url"] = self.get_github_url(context["version"])
context["github_url"] = (
library_version.library_repo_url_for_version
if library_version
else self.object.github_url
)
context["authors"] = self.get_related(library_version, "authors")
context["maintainers"] = self.get_related(
library_version,
@@ -312,7 +289,7 @@ class LibraryDetail(FormMixin, VersionAlertMixin, DetailView):
if getattr(x.commitauthor, "id", None)
]
top_contributors_release = self.get_top_contributors(
version=context["version"],
version=context["selected_version"],
exclude=exclude_maintainer_ids + exclude_author_ids,
)
context["top_contributors_release_new"] = [
@@ -323,7 +300,7 @@ class LibraryDetail(FormMixin, VersionAlertMixin, DetailView):
]
exclude_top_contributor_ids = [x.id for x in top_contributors_release]
context["previous_contributors"] = self.get_previous_contributors(
context["version"],
context["selected_version"],
exclude=exclude_maintainer_ids
+ exclude_top_contributor_ids
+ exclude_author_ids,
@@ -334,10 +311,10 @@ class LibraryDetail(FormMixin, VersionAlertMixin, DetailView):
# Populate the library description
client = GithubAPIClient(repo_slug=self.object.github_repo)
context["description"] = (
self.object.get_description(client, tag=context["version"].name)
self.object.get_description(client, tag=context["selected_version"].name)
or README_MISSING
)
context["library_view_str"] = get_prioritized_library_view(self.request)
return context
def get_commit_data_by_release(self):
@@ -361,16 +338,16 @@ class LibraryDetail(FormMixin, VersionAlertMixin, DetailView):
"""Get the current library object from the slug in the URL.
If present, use the version_slug to get the right LibraryVersion of the library.
Otherwise, default to the most recent version."""
slug = self.kwargs.get("slug")
library_slug = self.kwargs.get("library_slug")
version = self.get_version()
if not LibraryVersion.objects.filter(
version=version, library__slug__iexact=slug
version=version, library__slug__iexact=library_slug
).exists():
raise Http404("No library found matching the query")
try:
obj = self.get_queryset().get(slug__iexact=slug)
obj = self.get_queryset().get(slug__iexact=library_slug)
except self.model.DoesNotExist:
raise Http404("No library found matching the query")
return obj
@@ -400,13 +377,6 @@ class LibraryDetail(FormMixin, VersionAlertMixin, DetailView):
return commit_data_list
def get_current_library_version(self, version):
"""Return the library-version for the latest version of Boost"""
# Avoid raising an error if the library has been removed from the latest version
return LibraryVersion.objects.filter(
library=self.object, version=version
).first()
def get_github_url(self, version):
"""Get the GitHub URL for the current library."""
try:
@@ -498,10 +468,11 @@ class LibraryDetail(FormMixin, VersionAlertMixin, DetailView):
def get_version(self):
"""Get the version of Boost for the library we're currently looking at."""
version_slug = self.kwargs.get("version_slug")
if version_slug:
return get_object_or_404(Version, slug=version_slug)
else:
# here we need to check for not version_slug because of redirect_to_docs
# where it's not necessarily set by the source request
if not version_slug or version_slug == LATEST_RELEASE_URL_PATH_STR:
return Version.objects.most_recent()
return get_object_or_404(Version, slug=version_slug)
def dispatch(self, request, *args, **kwargs):
"""Redirect to the documentation page, if configured to."""
@@ -509,7 +480,7 @@ class LibraryDetail(FormMixin, VersionAlertMixin, DetailView):
return redirect(
get_documentation_url(
LibraryVersion.objects.get(
library__slug=self.kwargs.get("slug"),
library__slug=self.kwargs.get("library_slug"),
version=self.get_version(),
),
latest=True,
@@ -517,44 +488,6 @@ class LibraryDetail(FormMixin, VersionAlertMixin, DetailView):
)
response = super().dispatch(request, *args, **kwargs)
set_selected_boost_version(
kwargs.get("version_slug", LATEST_RELEASE_URL_PATH_STR), response
self.kwargs.get("version_slug", LATEST_RELEASE_URL_PATH_STR), response
)
return response
def post(self, request, *args, **kwargs):
"""User has submitted a form and will be redirected to the right record."""
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
version = form.cleaned_data["version"]
return redirect(
"library-detail-by-version",
version_slug=version.slug,
slug=self.object.slug,
)
else:
logger.info("library_list_invalid_version")
return redirect(request.get_full_path())
return super().get(request)
def render_to_response(self, context):
if self.object.slug != self.kwargs["slug"]:
# redirect to canonical case
try:
url = urls.reverse(
"library-detail-by-version",
kwargs={
"slug": self.object.slug,
"version_slug": self.kwargs["version_slug"],
},
)
except KeyError:
url = urls.reverse(
"library-detail",
kwargs={
"slug": self.object.slug,
},
)
return HttpResponseRedirect(url)
else:
return super().render_to_response(context)

View File

@@ -186,21 +186,56 @@
}
}
const versionResetForm = (event) => {
if (event.persisted) {
const form = document.getElementById('id_version').closest('form');
if (form) {
form.reset();
const changeVersionAndCategory = (event) => {
const urlPatterns = {
libraries: {
regex: /^(https?:\/\/[\S:]+\/libraries\/)([^\/]+)(\/?[a-z]*)(\/?\S*)?\/?$/,
substitution: (event) => {
switch (event.target.id) {
case 'id_category':
return `$1$2$3${event.target.value ? `/${event.target.value}` : ''}/`;
case 'id_version':
return `$1${event.target.value}$3$4`;
default:
return null;
}
}
},
library: {
regex: /^(https?:\/\/[\S:]+\/library\/)([^\/]+)\/([^\/]+)\/?$/,
substitution: (event) => `$1${event.target.value}/$3/`
},
releases: {
regex: /^(https?:\/\/[\S:]+\/releases\/)([^\/]+)?\/?$/,
substitution: (event) => `$1${event.target.value}/`
}
};
const currentUrl = window.location.href;
for (const key in urlPatterns) {
const pattern = urlPatterns[key];
if (pattern.regex.test(currentUrl)) {
const substitutionString = pattern.substitution(event);
if (substitutionString) {
window.location.href = currentUrl.replace(pattern.regex, substitutionString);
}
break;
}
}
}
window.addEventListener('DOMContentLoaded', () => {
// resets the form on back button press
document.getElementById("id_category")?.closest('form').reset();
document.getElementById("id_version")?.closest('form').reset();
document.getElementById("id_category")?.addEventListener("change", changeVersionAndCategory);
document.getElementById("id_version")?.addEventListener("change", changeVersionAndCategory);
});
(async () => {
await trackLoginUpdateCheck();
await delay(messageVisibilitySeconds * 1000);
await hideMessage();
})();
window.addEventListener('pageshow', versionResetForm);
</script>
</body>

View File

@@ -144,7 +144,7 @@
All Libraries&nbsp;<i class="fas fa-chevron-right text-sky-600 dark:text-sky-300 group-hover:text-orange dark:group-hover:text-orange"></i>
</a>
</div>
<h3 class="pb-2 mb-4 text-lg md:text-2xl lg:text-3xl capitalize border-b border-gray-400 text-orange dark:border-slate"><a href="{% url 'library-detail' slug=featured_library.slug %}" class="link-header">{{ featured_library.name }}</a></h3>
<h3 class="pb-2 mb-4 text-lg md:text-2xl lg:text-3xl capitalize border-b border-gray-400 text-orange dark:border-slate"><a href="{% url 'library-detail' library_slug=featured_library.slug version_slug=LATEST_RELEASE_URL_PATH_STR %}" class="link-header">{{ featured_library.name }}</a></h3>
<span class="pb-1 mx-auto w-full text-sm md:text-base align-left">
{{ featured_library.description }}
</span>

View File

@@ -390,7 +390,7 @@ html.dark {
</div>
<div class="right-menubar" x-data="{ 'searchOpen': false }">
<span style="position: relative;" x-ref="desktopSearchArea">
<i id="gecko-search-button" data-current-boost-version="{{ current_release.stripped_boost_url_slug }}" data-theme-mode="light" data-font-family="sans-serif" class="fas fa-search icon-link"></i>
<i id="gecko-search-button" data-current-boost-version="{{ current_version.stripped_boost_url_slug }}" data-theme-mode="light" data-font-family="sans-serif" class="fas fa-search icon-link"></i>
<script>
const geckoSearchButton = document.getElementById('gecko-search-button');
geckoSearchButton.setAttribute('data-theme-mode', localStorage.getItem('colorMode') === 'dark' ? 'dark' : 'light');

View File

@@ -1,13 +1,8 @@
<tr class="border-0 md:border border-gray-200/10 border-dotted md:border-t-0 md:border-r-0 md:border-l-0 md:border-b-1 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 ease-in-out cursor-pointer"
onclick="window.location='{% if version_str != LATEST_RELEASE_URL_PATH_STR %}{% url 'library-detail-by-version' slug=library_version.library.slug version_slug=version.slug %}{% else %}{% url 'library-detail' slug=library_version.library.slug %}{% endif %}'">
onclick="window.location='{% url 'library-detail' library_slug=library_version.library.slug version_slug=version_str %}'">
<td class="py-2 align-top md:w-1/5">
<a class="mr-1 font-bold capitalize text-sky-600 dark:text-sky-300 hover:text-orange dark:hover:text-orange"
href="
{% if version_str != LATEST_RELEASE_URL_PATH_STR %}
{% url 'library-detail-by-version' slug=library_version.library.slug version_slug=version.slug %}
{% else %}
{% url 'library-detail' slug=library_version.library.slug %}
{% endif %}"
href="{% url 'library-detail' library_slug=library_version.library.slug version_slug=version_str %}"
>{{ library_version.library.name }}</a>
</td>

View File

@@ -2,17 +2,11 @@
{% load date_filters %}
<div class="relative content-between p-3 bg-white md:rounded-lg md:shadow-lg md:p-5 dark:bg-charcoal hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-200 ease-in-out cursor-pointer"
onclick="window.location='{% if version_str != LATEST_RELEASE_URL_PATH_STR %}{% url 'library-detail-by-version' slug=library_version.library.slug version_slug=version.slug %}{% else %}{% url 'library-detail' slug=library_version.library.slug %}{% endif %}'">
onclick="window.location='{% url 'library-detail' library_slug=library_version.library.slug version_slug=version_str %}'">
<div class="">
<h3 class="pb-2 text-xl md:text-2xl capitalize border-b border-gray-700">
<a class="link-header" href="
{% if version_str != LATEST_RELEASE_URL_PATH_STR %}
{% url 'library-detail-by-version' slug=library_version.library.slug version_slug=version.slug %}
{% else %}
{% url 'library-detail' slug=library_version.library.slug %}
{% endif %}"
>{{ library_version.library.name }}</a>
{% for author in library_version.library.authors.all %}
<a class="link-header" href="{% url 'library-detail' library_slug=library_version.library.slug version_slug=version_str %}">{{ library_version.library.name }}</a>
{% for author in library.authors.all %}
{% if author.image %}
<img src="{{ author.image.url }}" class="inline float-right rounded w-[30px] ml-1" alt="{{ author.get_display_name }}" />
{% endif %}
@@ -34,7 +28,7 @@ onclick="window.location='{% if version_str != LATEST_RELEASE_URL_PATH_STR %}{%
{# <div class="w-1/6 tracking-wider text-charcoal dark:text-white/60">{% if library_version.library.first_boost_version %}{{ library_version.library.first_boost_version.release_date|years_since }} yrs{% endif %}</div>#}
<div class="w-5/6 text-right mr-2 font-bold capitalize text-sky-600 dark:text-sky-300">
{% for c in library_version.library.categories.all %}
<a href="{% url 'libraries' %}?category={{ c.slug }}{% if version_str != LATEST_RELEASE_URL_PATH_STR %}&version={{ version.slug }}{% endif %}" class="hover:text-orange">{{ c.name }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}
<a href="{% url 'libraries-list' library_view_str='grid' category_slug=c.slug version_slug=version_str %}" class="hover:text-orange">{{ c.name }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}
</div>
</div>
</div>

View File

@@ -1,14 +1,8 @@
<tr class="border-0 md:border border-gray-200/10 border-dotted md:border-t-0 md:border-r-0 md:border-l-0 md:border-b-1 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-300 ease-in-out cursor-pointer"
onclick="window.location='{% if version %}{% url 'library-detail-by-version' slug=library_version.library.slug version_slug=version.slug %}{% else %}{% url 'library-detail' slug=library_version.library.slug %}{% endif %}'">
onclick="window.location='{% url 'library-detail' library_slug=library_version.library.slug version_slug=version_str %}'">
<td class="py-2 align-top md:w-1/5">
<a class="mr-1 pl-1 font-bold capitalize text-sky-600 dark:text-sky-300 hover:text-orange dark:hover:text-orange"
href="
{% if version_str != LATEST_RELEASE_URL_PATH_STR %}
{% url 'library-detail-by-version' slug=library_version.library.slug version_slug=version.slug %}
{% else %}
{% url 'library-detail' slug=library_version.library.slug %}
{% endif %}"
>{{ library_version.library.name }}</a>
href="{% url 'library-detail' library_slug=library_version.library.slug version_slug=version_str %}">{{ library_version.library.name }}</a>
</td>
<td class="py-2 px-2 align-top w-12">

View File

@@ -21,7 +21,7 @@
<table class="table-auto w-full">
<tbody>
{% for library_version in result.library_version_list %}
{% include "libraries/_library_category_list_item.html" %}
{% include "libraries/_library_categorized_list_item.html" %}
{% empty %}
<p class="text-gray-600 dark:text-gray-400">No libraries in this category yet.</p>
{% endfor %}

View File

@@ -1,5 +1,5 @@
{% extends "base.html" %}
{% load i18n static avatar_tags %}
{% load i18n static avatar_tags version_select %}
{% block title %}{{ object.display_name }} ({{ version.display_name }}){% endblock %}
{% block description %}{% if library_version.description %}{% trans library_version.description %}{% endif %}{% endblock %}
@@ -17,21 +17,7 @@
</div>
</div>
<div class="flex-shrink mr-3 md:mr-0">
<form action="{% url 'library-detail' slug=object.slug %}"
method="post">
<div>
<label for="id_version" hidden="true">Versions:</label>
<select onchange="this.form.submit()"
name="version"
class="dropdown pb-0"
id="id_version">
<option value="{{ LATEST_RELEASE_URL_PATH_STR }}" {% if version_str == LATEST_RELEASE_URL_PATH_NAME %}selected="selected"{% endif %}>Latest</option>
{% for v in versions %}
<option value="{{ v.pk }}" {% if version_str == v.slug %}selected="selected"{% endif %}>{{ v.display_name }}</option>
{% endfor %}
</select>
</div>
</form>
{% version_select %}
</div>
</div>
@@ -59,7 +45,7 @@
<span class="font-bold">Categories:</span>
{% for category in object.categories.all %}
<a class="inline text-sky-600 dark:text-sky-300 hover:text-orange dark:hover:text-orange"
href="{% url 'libraries' %}?category={{ category.slug }}{% if version_str != LATEST_RELEASE_URL_PATH_NAME %}&version={{ version.slug }}{% endif %}">{{ category.name }}</a>
href="{% url 'libraries-list' category_slug=category.slug library_view_str=library_view_str version_slug=version_str %}">{{ category.name }}</a>
{% if not forloop.last %}, {% endif %}
{% endfor %}
</div>

View File

@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Boost Libraries" %}{% endblock %}
{% block description %}{% trans "Explore our comprehensive list of Boost C++ Libraries and discover tools for multithreading, image processing, testing, and more." %}{% endblock %}
{% block content %}
<main class="content">
{% include "libraries/includes/library_preferences.html" %}
{% if object_list %}
{# alert for non-current Boost versions #}
{% include "libraries/includes/version_alert.html" %}
{# Libraries list #}
<div class="grid grid-cols-1 gap-4 mb-5 md:grid-cols-2 lg:grid-cols-3">
{% for library_version in object_list %}
{% include "libraries/_library_grid_list_item.html" %}
{% endfor %}
</div>
{# end libraries list #}
{% if page_obj.paginator %}
{# pagination #}
<div class="space-x-3 text-center">
{% if page_obj.has_previous %}
<a href="?page=1" class="text-orange"><small> &lt;&lt; First</small></a>
<a href="?page={{ page_obj.previous_page_number }}" class="text-orange"><small> &lt; Previous</small> </a>
{% endif %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}" class="text-orange"><small>Next <small> &gt; </small></a>
<a href="?page={{ page_obj.paginator.num_pages }}" class="text-orange">Last <small> &gt;&gt;</small></a>
{% endif %}
</div>
{# end pagination #}
{% endif %}
{% else %}
<div>
No library records available at this time. Check back later.
</div>
{% endif %}
</main>
{% endblock %}

View File

@@ -1,4 +1,4 @@
{% load version_select %}
{% load version_select %}
{% with request.resolver_match.view_name as view_name %}
<div class="pt-3 px-0 mb-2 text-right md:mb-2 mx-3 md:mx-0">
<form action="{{request.path}}" method="get">
@@ -10,15 +10,15 @@
{# Display options #}
<div class="flex space-x-3">
<div class="relative group">
<a title="Name View" href="{% url 'libraries-mini' %}{% if url_params %}?{{ request.GET.urlencode }}{% endif %}"><i class="link rounded border border-gray-300 cursor-pointer fas fa-list p-[10px] {% if view_name == 'libraries-mini' %}bg-gray-100 dark:bg-slate{% else %}hover:bg-gray-100 dark:hover:bg-slate{% endif %}"></i></a>
<a title="Name View" href="{% if category_slug %}{% url 'libraries-list' library_view_str='list' version_slug=version_str category_slug=category_slug %}{% else %}{% url 'libraries-list' library_view_str='list' version_slug=version_str %}{% endif %}"><i class="link rounded border border-gray-300 cursor-pointer fas fa-list p-[10px] {% if library_view_str == 'list' %}bg-gray-100 dark:bg-slate{% else %}hover:bg-gray-100 dark:hover:bg-slate{% endif %}"></i></a>
<span class="z-50 group-hover:opacity-100 transition-opacity bg-slate px-1 text-xs text-gray-100 rounded-sm absolute top-5 left-1/2 -translate-x-1/2 translate-y-full opacity-0 m-0 mx-auto w-auto">List&nbsp;View</span>
</div>
<div class="relative group">
<a title="Grid View" href="{% url 'libraries-grid' %}{% if url_params %}?{{ request.GET.urlencode }}{% endif %}"><i class="link rounded border border-gray-300 cursor-pointer fas fa-th-large p-[10px] {% if view_name == 'libraries-grid' %}bg-gray-100 dark:bg-slate{% else %}hover:bg-gray-100 dark:hover:bg-slate{% endif %}"></i></a>
<a title="Grid View" href="{% if category_slug %}{% url 'libraries-list' library_view_str='grid' version_slug=version_str category_slug=category_slug %}{% else %}{% url 'libraries-list' library_view_str='grid' version_slug=version_str %}{% endif %}"><i class="link rounded border border-gray-300 cursor-pointer fas fa-th-large p-[10px] {% if library_view_str == 'grid' %}bg-gray-100 dark:bg-slate{% else %}hover:bg-gray-100 dark:hover:bg-slate{% endif %}"></i></a>
<span class="z-50 group-hover:opacity-100 transition-opacity bg-slate px-1 text-xs text-gray-100 rounded-sm absolute top-5 left-1/2 -translate-x-1/2 translate-y-full opacity-0 m-0 mx-auto w-auto">Grid&nbsp;View</span>
</div>
<div class="relative group">
<a title="Category View" href="{% url 'libraries-by-category' %}{% if url_params %}?{{ request.GET.urlencode }}{% endif %}"><i class="link rounded border border-gray-300 cursor-pointer fas fa-cat p-[10px] {% if view_name == 'libraries-by-category' %}bg-gray-100 dark:bg-slate{% else %}hover:bg-gray-100 dark:hover:bg-slate{% endif %}"></i></a>
<a title="Category View" href="{% if category_slug %}{% url 'libraries-list' library_view_str='categorized' version_slug=version_str category_slug=category_slug %}{% else %}{% url 'libraries-list' library_view_str='categorized' version_slug=version_str %}{% endif %}"><i class="link rounded border border-gray-300 cursor-pointer fas fa-cat p-[10px] {% if library_view_str == 'categorized' %}bg-gray-100 dark:bg-slate{% else %}hover:bg-gray-100 dark:hover:bg-slate{% endif %}"></i></a>
<span class="z-50 group-hover:opacity-100 transition-opacity bg-slate px-1 text-xs text-gray-100 rounded-sm absolute top-5 left-1/2 -translate-x-1/2 translate-y-full opacity-0 m-0 mx-auto w-auto">Category&nbsp;View</span>
</div>
</div>
@@ -26,22 +26,20 @@
<div></div>
{# Select a category #}
{% if view_name != 'libraries-by-category' %}
<div>
{# todo: if someone selects a category and hits back, it retains their choice here. #}
<select onchange="this.form.submit()"
<div>
<select
name="category"
class="block py-2 pr-11 pl-5 mb-3 w-full text-sm bg-white rounded-md border border-gray-300 cursor-pointer sm:inline-block md:mb-0 ml-3 md:ml-0 md:w-auto dark:bg-black dark:border-slate"
class="block py-2 pr-11 pl-5 mb-3 w-full text-sm bg-white rounded-md border border-gray-300 cursor-pointer sm:inline-block md:mb-0 ml-3 md:ml-0 md:w-auto dark:bg-black dark:border-slate disabled:dark:"
id="id_category"
{% if library_view_str == 'categorized' %}disabled="disabled"{% endif %}
>
<option value="">Filter by category</option>
{% for c in categories %}
<option value="{{ c.slug }}" {% if category == c %}selected="selected"{% endif %}>{{ c.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{# Select a version #}
<div class="flex grow justify-end">
{% version_select %}

View File

@@ -1,6 +1,6 @@
{% for library in libraries %}
<div class="bg-white dark:bg-gray-800 flex py-2 px-3 cursor-pointer select-none group" @click="document.location.href = '{% url 'library-detail' slug=library.slug %}';">
<a href="{% url 'library-detail' slug=library.slug %}" class="group-hover:text-orange">
<div class="bg-white dark:bg-gray-800 flex py-2 px-3 cursor-pointer select-none group" @click="document.location.href = '{% url 'library-detail' library_slug=library.slug version_slug='latest' %}';">
<a href="{% url 'library-detail' library_slug=library.slug version_slug='latest' %}" class="group-hover:text-orange">
<span class="">
{{ library.name|capfirst }}
</span>

View File

@@ -2,17 +2,17 @@
<div role="alert" class="py-2 px-3 mb-3 text-center rounded-sm bg-yellow-200/70">
<p class="p-0 m-0">
<i class="fas fa-exclamation-circle"></i>
{% if version == current_release %}
You've currently chosen the {{ current_release.display_name }} version. If a newer release comes out, you will continue to view the {{ current_release.display_name }} version, not the new <a href="{{ version_alert_url }}" class="font-semibold underline dark:text-white text-charcoal">latest release</a>.
{% if selected_version == current_version %}
You've currently chosen the {{ current_version.display_name }} version. If a newer release comes out, you will continue to view the {{ current_version.display_name }} version, not the new <a href="{{ version_alert_url }}" class="font-semibold underline dark:text-white text-charcoal">latest release</a>.
{% else %}
{% if version.beta %}
{% if selected_version.beta %}
This is a beta version of Boost.
{% elif version.full_release %}
This is an older version and was released in {{ version.release_date|date:"Y"}}.
This is an older version and was released in {{ selected_version.release_date|date:"Y"}}.
{% else %}
This version of Boost is under active development.
{% endif %}
The <a href="{{ version_alert_url }}" class="font-semibold underline dark:text-white text-charcoal">current version</a> is {{ current_release.display_name }}.
The <a href="{{ version_alert_url }}" class="font-semibold underline dark:text-white text-charcoal">current version</a> is {{ current_version.display_name }}.
{% endif %}
</p>
</div>

View File

@@ -1,41 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Boost Libraries" %}{% endblock %}
{% block description %}{% trans "Explore our comprehensive list of Boost C++ Libraries and discover tools for multithreading, image processing, testing, and more." %}{% endblock %}
{% block content %}
<main class="content">
{% if library_list %}
{% include "libraries/includes/library_preferences.html" %}
{# alert for non-current Boost versions #}
{% include "libraries/includes/version_alert.html" %}
{# Libraries list #}
<div class="grid grid-cols-1 gap-4 mb-5 md:grid-cols-2 lg:grid-cols-3">
{% for library_version in library_version_list %}
{% include "libraries/_library_list_item.html" %}
{% endfor %}
</div>
{# end libraries list #}
{% if page_obj.paginator %}
{# pagination #}
<div class="space-x-3 text-center">
{% if page_obj.has_previous %}
<a href="?page=1" class="text-orange"><small> &lt;&lt; First</small></a>
<a href="?page={{ page_obj.previous_page_number }}" class="text-orange"><small> &lt; Previous</small> </a>
{% endif %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}" class="text-orange"><small>Next <small> &gt; </small></a>
<a href="?page={{ page_obj.paginator.num_pages }}" class="text-orange">Last <small> &gt;&gt;</small></a>
{% endif %}
</div>
{# end pagination #}
{% endif %}
{% endif %}
</main>
{% endblock %}

View File

@@ -20,8 +20,8 @@
{% endif %}
<table class="table-auto w-full">
<tbody>
{% for library_version in library_version_list %}
{% include "libraries/_library_flat_list_item.html" %}
{% for library_version in object_list %}
{% include "libraries/_library_vertical_list_item.html" %}
{% endfor %}
</tbody>
</table>

View File

@@ -1,15 +1,18 @@
{% load boost_version %}
<form action="." method="get">
<select onchange="this.form.submit()"
name="version"
class="dropdown !mb-0 h-[38px]"
id="id_version">
<select
name="version"
class="dropdown !mb-0 h-[38px]"
id="id_version"
>
<option value="{{ LATEST_RELEASE_URL_PATH_STR }}"
{% if version_str == LATEST_RELEASE_URL_PATH_STR %}selected="selected"{% endif %}>
Latest
</option>
{% for v in versions %}
<option value="{{ v.slug }}"
{% if version_str == v.slug %}selected="selected"{% endif %}>
<option value="{{ v.slug|boost_version }}"
{% if version_str == v.slug %}selected="selected"{% endif %}
>
{{ v.display_name }}
</option>
{% endfor %}

View File

@@ -3,11 +3,12 @@
{% load static %}
{% load text_helpers %}
{% load avatar_tags %}
{% load version_select %}
{% block title %}{% blocktrans with version_name=version.display_name %}Boost {{ version_name }}{% endblocktrans %}{% endblock %}
{% block description %}{% blocktrans with version_name=version.display_name %}Discover what's new in Boost {{ version_name }}{% endblocktrans %}{% endblock %}
{% block content %}
<main class="content">
{% if version %}
{% if selected_version %}
<div class="py-3 px-3 md:mt-3 md:px-0 mb-0 w-full flex flex-row flex-nowrap items-center"
x-data="{'showSearch': false}"
x-on:keydown.escape="showSearch=false">
@@ -20,22 +21,7 @@
</div>
</div>
<div class="flex-shrink text-right">
<form action="." method="post">
<div>
<select onchange="this.form.submit()"
name="version"
class="dropdown !mb-0 h-[38px]"
id="id_version">
<option value="{{ LATEST_RELEASE_URL_PATH_STR }}" {% if version_str == LATEST_RELEASE_URL_PATH_STR %}selected="selected"{% endif %}>Latest</option>
{% for v in versions %}
<option value="{{ v.pk }}"
{% if version_str == v.slug %}selected="selected"{% endif %}>
{{ v.display_name }}
</option>
{% endfor %}
</select>
</div>
</form>
{% version_select %}
</div>
</div>
<!-- alert for non-current Boost versions -->

29
versions/converters.py Normal file
View File

@@ -0,0 +1,29 @@
from libraries.constants import (
LATEST_RELEASE_URL_PATH_STR,
LEGACY_LATEST_RELEASE_URL_PATH_STR,
VERSION_SLUG_PREFIX,
)
def to_python(value):
if value in (LATEST_RELEASE_URL_PATH_STR, LEGACY_LATEST_RELEASE_URL_PATH_STR):
return LATEST_RELEASE_URL_PATH_STR
return f"{VERSION_SLUG_PREFIX}{value.replace('.', '-')}"
def to_url(value):
if value == LATEST_RELEASE_URL_PATH_STR:
return LATEST_RELEASE_URL_PATH_STR
if value:
value = value.replace(VERSION_SLUG_PREFIX, "").replace("-", ".")
return value
class BoostVersionSlugConverter:
regex = r"[a-zA-Z0-9\-\.]+"
def to_python(self, value):
return to_python(value)
def to_url(self, value):
return to_url(value)

View File

@@ -5,6 +5,7 @@ from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.text import slugify
from .converters import to_url
from .managers import VersionManager, VersionFileManager
User = get_user_model()
@@ -53,7 +54,7 @@ class Version(models.Model):
return super(Version, self).save(*args, **kwargs)
def get_absolute_url(self):
return reverse("release-detail", args=[str(self.slug)])
return reverse("release-detail", args=[to_url(str(self.slug))])
def get_slug(self):
if self.slug:

View File

View File

@@ -0,0 +1,8 @@
from django import template
register = template.Library()
@register.filter
def boost_version(slug):
return slug.replace("boost-", "").replace("-", ".")

View File

@@ -111,7 +111,7 @@ def test_stripped_boost_url_slug(slug, expected, version):
def test_get_absolute_url(version):
expected_url = f"/releases/{version.slug}/"
expected_url = f"/releases/{version.slug.replace('boost-', '').replace('-', '.')}/"
assert version.get_absolute_url() == expected_url

View File

@@ -12,7 +12,7 @@ def test_version_most_recent_detail(version, tp):
ten_years_ago = now - timedelta(days=365 * 10)
baker.make("versions.Version", name="boost-0.0.0", release_date=ten_years_ago)
res = tp.get_check_200("releases-most-recent")
res = tp.get_check_200("releases-most-recent", follow=True)
assert "versions" in res.context
assert res.context["version"] == version
@@ -22,20 +22,12 @@ def test_version_detail_no_data(tp):
GET /releases/
"""
Version.objects.all().delete()
tp.get_check_200("releases-most-recent")
tp.get_check_200("releases-most-recent", follow=True)
def test_version_detail(version, tp):
"""
GET /releases/{slug}/
GET /releases/{version_slug}/
"""
res = tp.get("release-detail", slug=version.slug)
tp.response_200(res)
def test_version_detail_post(version, tp):
"""
POST /releases/{slug}/
"""
res = tp.post("releases-most-recent", data={"version": version.slug})
res = tp.get("release-detail", version_slug=version.slug)
tp.response_200(res)

View File

@@ -1,5 +1,4 @@
from django.db.models.query import QuerySet
import structlog
from itertools import groupby
from operator import attrgetter
@@ -15,7 +14,7 @@ from django.views.decorators.csrf import csrf_exempt
from core.models import RenderedContent
from libraries.constants import LATEST_RELEASE_URL_PATH_STR
from libraries.forms import VersionSelectionForm
from libraries.mixins import VersionAlertMixin
from libraries.mixins import VersionAlertMixin, BoostVersionMixin
from libraries.models import Commit, CommitAuthor
from libraries.utils import (
set_selected_boost_version,
@@ -25,11 +24,8 @@ from libraries.utils import (
from versions.models import Review, Version
logger = structlog.get_logger(__name__)
@method_decorator(csrf_exempt, name="dispatch")
class VersionDetail(FormMixin, VersionAlertMixin, DetailView):
class VersionDetail(FormMixin, BoostVersionMixin, VersionAlertMixin, DetailView):
"""Web display of list of Versions"""
form_class = VersionSelectionForm
@@ -39,7 +35,8 @@ class VersionDetail(FormMixin, VersionAlertMixin, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data()
obj = self.get_object()
# .get_object() is called on /releases, with no version pk nor existing context
obj = context.get("selected_version") or self.get_object()
# Handle the case where no data has been uploaded
if not obj:
@@ -50,18 +47,18 @@ class VersionDetail(FormMixin, VersionAlertMixin, DetailView):
)
context["versions"] = None
context["downloads"] = None
context["current_release"] = None
context["selected_version"] = None
context["is_current_release"] = False
return context
context["versions"] = Version.objects.version_dropdown_strict()
downloads = obj.downloads.all().order_by("operating_system")
context["downloads"] = {
k: list(v)
for k, v in groupby(downloads, key=attrgetter("operating_system"))
}
obj = self.get_object()
context["heading"] = self.get_version_heading(
obj, context["current_release"] == obj
obj, context["current_version"] == obj
)
context["release_notes"] = self.get_release_notes(obj)
context["top_contributors_release"] = self.get_top_contributors_release(obj)
@@ -105,53 +102,26 @@ class VersionDetail(FormMixin, VersionAlertMixin, DetailView):
else:
return "Development Branch"
def post(self, request, *args, **kwargs):
"""User has submitted a form and will be redirected to the right record."""
form = self.get_form()
version_slug = self.request.POST.get("version")
if version_slug == LATEST_RELEASE_URL_PATH_STR:
response = redirect("releases-most-recent")
set_selected_boost_version(LATEST_RELEASE_URL_PATH_STR, response)
return response
elif form.is_valid():
version = form.cleaned_data["version"]
response = redirect(
"release-detail",
slug=version.slug,
)
set_selected_boost_version(version.slug, response)
return response
else:
logger.info("version_detail_invalid_version")
return super().get(request)
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
# if 'release' clear the version values, e.g. from version_alert
if self.kwargs.get("slug") == LATEST_RELEASE_URL_PATH_STR:
response = redirect("releases-most-recent")
set_selected_boost_version(LATEST_RELEASE_URL_PATH_STR, response)
return response
version = determine_selected_boost_version(
self.kwargs.get("slug"), self.request
)
if version != self.kwargs.get("slug"):
version_slug = self.kwargs.get("version_slug")
# if set in kwargs, update the cookie
if version_slug:
set_selected_boost_version(version_slug, response)
else:
version_slug = (
determine_selected_boost_version(version_slug, self.request)
or LATEST_RELEASE_URL_PATH_STR
)
response = redirect(
"release-detail",
slug=version,
version_slug=version_slug,
)
return response
def get_object(self, queryset=None):
"""Return the object that the view is displaying"""
if self.request.POST:
version_slug = self.request.POST.get("version")
else:
version_slug = self.kwargs.get("slug", LATEST_RELEASE_URL_PATH_STR)
version_slug = self.kwargs.get("version_slug", LATEST_RELEASE_URL_PATH_STR)
if version_slug == LATEST_RELEASE_URL_PATH_STR:
return Version.objects.most_recent()