Skip to content

Commit

Permalink
Merge pull request #1015 from thunderstore-io/dynamic-footer-links
Browse files Browse the repository at this point in the history
Add support for dynamic footer links
  • Loading branch information
MythicManiac authored Feb 19, 2024
2 parents 463a1ea + 5c9f9f9 commit e0ab3ee
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 24 deletions.
12 changes: 4 additions & 8 deletions builder/src/scss/footer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,10 @@

.footer_content_right {
display: flex;
gap: 5rem;

@media (max-width: 575px) {
flex-direction: column-reverse;
justify-content: center;
align-items: center;
gap: 2rem;
}
flex-wrap: wrap;
gap: 4rem;
margin-left: 4rem;
margin-right: 4rem;
}

.footer_column {
Expand Down
1 change: 1 addition & 0 deletions django/thunderstore/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ def load_db_certs():
"thunderstore.community.context_processors.selectable_communities",
"thunderstore.legal.context_processors.legal_contracts",
"thunderstore.frontend.context.nav_links",
"thunderstore.frontend.context.footer_links",
],
},
},
Expand Down
51 changes: 48 additions & 3 deletions django/thunderstore/frontend/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from django import forms
from django.contrib import admin

from thunderstore.frontend.models import CommunityNavLink, DynamicHTML, NavLink
from thunderstore.frontend.models import (
CommunityNavLink,
DynamicHTML,
FooterLink,
NavLink,
)


@admin.register(DynamicHTML)
Expand Down Expand Up @@ -30,7 +36,7 @@ class DynamicHTML(admin.ModelAdmin):


@admin.register(NavLink)
class NavLinkAdmin(admin.ModelAdmin):
class LinkAdmin(admin.ModelAdmin):
readonly_fields = (
"datetime_created",
"datetime_updated",
Expand All @@ -52,7 +58,7 @@ class NavLinkAdmin(admin.ModelAdmin):


@admin.register(CommunityNavLink)
class CommunityNavLinkAdmin(NavLinkAdmin):
class CommunityNavLinkAdmin(LinkAdmin):
raw_id_fields = ("community",)
list_display = (
"pk",
Expand All @@ -69,3 +75,42 @@ class CommunityNavLinkAdmin(NavLinkAdmin):
"title",
"href",
)


class FooterLinkAdminForm(forms.ModelForm):
class Meta:
model = FooterLink
widgets = {
"title": forms.TextInput(attrs={"size": 40}),
"group_title": forms.TextInput(attrs={"size": 40}),
"href": forms.TextInput(attrs={"size": 40}),
"css_class": forms.TextInput(attrs={"size": 40}),
}
fields = "__all__"


@admin.register(FooterLink)
class FooterLinkAdmin(LinkAdmin):
form = FooterLinkAdminForm
readonly_fields = (
"datetime_created",
"datetime_updated",
)
list_display = (
"pk",
"title",
"group_title",
"href",
"order",
"datetime_created",
"datetime_updated",
"is_active",
)
list_filter = (
"is_active",
"group_title",
)
search_fields = (
"title",
"href",
)
97 changes: 95 additions & 2 deletions django/thunderstore/frontend/context.py
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 django/thunderstore/frontend/migrations/0012_add_footer_links.py
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",
),
),
]
15 changes: 12 additions & 3 deletions django/thunderstore/frontend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ class LinkTargetChoices(TextChoices):
Top = "_top"


class NavLinkMixin(TimestampMixin):
class LinkMixin(TimestampMixin):
title = models.TextField()
href = models.TextField()
css_class = models.TextField(blank=True, null=True)
Expand All @@ -139,15 +139,15 @@ class Meta:
abstract = True


class NavLink(NavLinkMixin):
class NavLink(LinkMixin):
objects: Manager["NavLink"]

class Meta:
ordering = ("order", "title")
indexes = [models.Index(fields=["is_active", "order", "title"])]


class CommunityNavLink(NavLinkMixin):
class CommunityNavLink(LinkMixin):
objects: Manager["CommunityNavLink"]

community = models.ForeignKey(
Expand All @@ -161,4 +161,13 @@ class Meta:
indexes = [models.Index(fields=["community", "is_active", "order", "title"])]


class FooterLink(LinkMixin):
objects: Manager["FooterLink"]
group_title = models.TextField()

class Meta:
ordering = ("order", "title")
indexes = [models.Index(fields=["is_active", "group_title", "order", "title"])]


signals.post_save.connect(DynamicHTML.post_save, sender=DynamicHTML)
19 changes: 11 additions & 8 deletions django/thunderstore/frontend/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ <h6 class="title">Other communities</h6>
{% endblock %}
</div>
<div class="container footer_content">
<div class="footer_content_left mr-4">
<div class="footer_content_left">
<img class="thunderstore_logo" src="{% static 'ts-logo-horizontal.svg' %}" />
{% block footer_bottom %}
{% dynamic_html "footer_bottom" %}
Expand All @@ -203,20 +203,23 @@ <h6 class="title">Other communities</h6>
<div class="footer_content_right">
{% if legal_contracts %}
<div class="footer_column">
<h2>About</h2>
<h2>Legal</h2>
{% for contract in legal_contracts %}
<a href="{{ contract.get_absolute_url }}">{{ contract.title }}</a>
{% endfor %}
</div>
{% endif %}
{% for group in footer_link_groups %}
<div class="footer_column">
<h2>Developers</h2>
<a href="{% url 'swagger' %}">API Docs</a>
<a href="https://github.com/thunderstore-io/Thunderstore">GitHub Repo</a>
<a href="{% community_url 'packages.create.docs' %}">Package Format Docs</a>
<a href="{% url 'tools.markdown-preview' %}">Markdown Preview</a>
<a href="{% url 'tools.manifest-v1-validator' %}">Manifest Validator</a>
<h2>{{ group.title }}</h2>
{% for link in group.links %}
<a
{% if link.css_class %}class="{{ link.css_class }}"{% endif %}
{% if link.target %}target="{{ link.target }}"{% endif %}
href="{{ link.href }}">{{ link.title }}</a>
{% endfor %}
</div>
{% endfor %}
</div>
{% endcache %}
</div>
Expand Down
43 changes: 43 additions & 0 deletions django/thunderstore/frontend/tests/test_footer_links.py
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

0 comments on commit e0ab3ee

Please sign in to comment.