-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1015 from thunderstore-io/dynamic-footer-links
Add support for dynamic footer links
- Loading branch information
Showing
8 changed files
with
271 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,102 @@ | ||
from thunderstore.frontend.models import NavLink | ||
from __future__ import annotations | ||
|
||
from collections import defaultdict | ||
from typing import TYPE_CHECKING, Dict, List, TypedDict | ||
|
||
from django.urls import reverse | ||
from django.utils.functional import SimpleLazyObject | ||
|
||
from thunderstore.frontend.models import FooterLink, NavLink | ||
|
||
if TYPE_CHECKING: | ||
from typing import NotRequired | ||
|
||
|
||
def nav_links(request): | ||
return { | ||
# This is implicitly ordered based on the model's default ordering, | ||
# which follows the db index. | ||
"global_nav_links": NavLink.objects.filter(is_active=True) | ||
"global_nav_links": NavLink.objects.filter(is_active=True), | ||
} | ||
|
||
|
||
class FooterLinkData(TypedDict): | ||
href: str | ||
title: str | ||
css_class: "NotRequired[str | None]" | ||
target: "NotRequired[str | None]" | ||
|
||
|
||
class FooterLinkGroup(TypedDict): | ||
title: str | ||
links: List[FooterLinkData] | ||
|
||
|
||
developer_links = [ | ||
{ | ||
"href": reverse("swagger"), | ||
"title": "API Docs", | ||
}, | ||
{ | ||
"href": "https://github.com/thunderstore-io/Thunderstore", | ||
"title": "GitHub Repo", | ||
}, | ||
{ | ||
"href": reverse("old_urls:packages.create.docs"), | ||
"title": "Package Format Docs", | ||
}, | ||
{ | ||
"href": reverse("tools.markdown-preview"), | ||
"title": "Markdown Preview", | ||
}, | ||
{ | ||
"href": reverse("tools.manifest-v1-validator"), | ||
"title": "Manifest Validator", | ||
}, | ||
] | ||
|
||
|
||
def build_footer_links() -> List[FooterLinkGroup]: | ||
link_groups: Dict[str, List[FooterLinkData]] = defaultdict( | ||
list, | ||
{ | ||
"Developers": list(developer_links), | ||
}, | ||
) | ||
|
||
# Implicity sorted correctly in the db + we don't care about | ||
# re-sorting if the `Developers` column is appended as a design | ||
# choice. Nothing breaks if that choice is changed later, it's | ||
# just a tiny amount more performant. | ||
entry: FooterLink | ||
for entry in FooterLink.objects.filter(is_active=True): | ||
link_groups[entry.group_title].append( | ||
{ | ||
"href": entry.href, | ||
"title": entry.title, | ||
"css_class": entry.css_class, | ||
"target": entry.target, | ||
} | ||
) | ||
|
||
return sorted( | ||
[ | ||
{ | ||
"title": key, | ||
"links": values, | ||
} | ||
for key, values in link_groups.items() | ||
], | ||
key=lambda x: x["title"], | ||
) | ||
|
||
|
||
def footer_links(request): | ||
# The template fragment is cached so we should make the context processor | ||
# return a lazy object to avoid DB queries when re-rendering the template | ||
# isn't needed. | ||
result = SimpleLazyObject(build_footer_links) | ||
|
||
return { | ||
"footer_link_groups": result, | ||
} |
57 changes: 57 additions & 0 deletions
57
django/thunderstore/frontend/migrations/0012_add_footer_links.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# Generated by Django 3.1.7 on 2024-02-17 03:39 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
("frontend", "0011_add_package_page_placeholder"), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name="FooterLink", | ||
fields=[ | ||
( | ||
"id", | ||
models.AutoField( | ||
auto_created=True, | ||
primary_key=True, | ||
serialize=False, | ||
verbose_name="ID", | ||
), | ||
), | ||
("datetime_created", models.DateTimeField(auto_now_add=True)), | ||
("datetime_updated", models.DateTimeField(auto_now=True)), | ||
("title", models.TextField()), | ||
("href", models.TextField()), | ||
("css_class", models.TextField(blank=True, null=True)), | ||
( | ||
"target", | ||
models.TextField( | ||
choices=[ | ||
("_blank", "Blank"), | ||
("_parent", "Parent"), | ||
("_self", "Self"), | ||
("_top", "Top"), | ||
], | ||
default="_self", | ||
), | ||
), | ||
("order", models.IntegerField(default=0)), | ||
("is_active", models.BooleanField(default=True)), | ||
("group_title", models.TextField()), | ||
], | ||
options={ | ||
"ordering": ("order", "title"), | ||
}, | ||
), | ||
migrations.AddIndex( | ||
model_name="footerlink", | ||
index=models.Index( | ||
fields=["is_active", "group_title", "order", "title"], | ||
name="frontend_fo_is_acti_3fbacf_idx", | ||
), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
from io import BytesIO | ||
|
||
import pytest | ||
from django.test import Client | ||
from lxml import etree | ||
|
||
from thunderstore.community.models import CommunitySite | ||
from thunderstore.frontend.models import FooterLink, LinkTargetChoices | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_frontend_footer_links(client: Client, community_site: CommunitySite): | ||
test_link = FooterLink.objects.create( | ||
title="Test link", | ||
group_title="Test group", | ||
href="http://example.com", | ||
target=LinkTargetChoices.Blank, | ||
) | ||
inactive_link = FooterLink.objects.create( | ||
title="Inactive link", | ||
is_active=False, | ||
group_title=test_link.group_title, | ||
href="http://example.org", | ||
target=LinkTargetChoices.Blank, | ||
) | ||
response = client.get( | ||
f"/c/{community_site.community.identifier}/", | ||
HTTP_HOST=community_site.site.domain, | ||
) | ||
assert response.status_code == 200 | ||
buffer = BytesIO(response.content) | ||
tree = etree.parse(buffer, etree.HTMLParser()) | ||
groups = tree.xpath( | ||
f"//div[@class='footer_column']/h2[contains(text(), '{test_link.group_title}')]" | ||
) | ||
assert len(groups) == 1 | ||
group_container = groups[0].getparent() | ||
links = group_container.xpath("./a") | ||
assert len(links) == 1 | ||
link = links[0] | ||
assert link.attrib["href"] == test_link.href | ||
assert link.attrib["target"] == test_link.target | ||
assert link.text == test_link.title |