diff options
| -rw-r--r-- | pydis_site/apps/content/urls.py | 12 | ||||
| -rw-r--r-- | pydis_site/apps/content/utils.py | 101 | ||||
| -rw-r--r-- | pydis_site/apps/content/views/page_category.py | 5 | ||||
| -rw-r--r-- | pydis_site/apps/content/views/tags.py | 79 | ||||
| -rw-r--r-- | pydis_site/static/css/content/color.css | 7 | ||||
| -rw-r--r-- | pydis_site/static/css/content/tag.css | 8 | ||||
| -rw-r--r-- | pydis_site/static/js/content/listing.js | 5 | ||||
| -rw-r--r-- | pydis_site/templates/content/listing.html | 17 | ||||
| -rw-r--r-- | pydis_site/templates/content/tag.html | 5 | 
9 files changed, 187 insertions, 52 deletions
| diff --git a/pydis_site/apps/content/urls.py b/pydis_site/apps/content/urls.py index b4ffc07d..03c0015a 100644 --- a/pydis_site/apps/content/urls.py +++ b/pydis_site/apps/content/urls.py @@ -39,15 +39,21 @@ def get_all_pages() -> DISTILL_RETURN:  def get_all_tags() -> DISTILL_RETURN: -    """Return all tag names in the repository in static builds.""" +    """Return all tag names and groups in static builds.""" +    groups = {None}      for tag in utils.get_tags_static(): -        yield {"name": tag.name} +        groups.add(tag.group) +        yield {"location": (f"{tag.group}/" if tag.group else "") + tag.name} + +    groups.remove(None) +    for group in groups: +        yield {"location": group}  urlpatterns = [      distill_path("", views.PageOrCategoryView.as_view(), name='pages'),      distill_path( -        "tags/<str:name>/", +        "tags/<path:location>/",          views.TagView.as_view(),          name="tag",          distill_func=get_all_tags diff --git a/pydis_site/apps/content/utils.py b/pydis_site/apps/content/utils.py index cc08f81f..da6a024d 100644 --- a/pydis_site/apps/content/utils.py +++ b/pydis_site/apps/content/utils.py @@ -2,6 +2,7 @@ import datetime  import functools  import tarfile  import tempfile +import typing  from io import BytesIO  from pathlib import Path @@ -16,7 +17,6 @@ from markdown.extensions.toc import TocExtension  from pydis_site import settings  from .models import Tag -TAG_URL_BASE = "https://github.com/python-discord/bot/tree/main/bot/resources/tags"  TAG_CACHE_TTL = datetime.timedelta(hours=1) @@ -44,9 +44,13 @@ def get_tags_static() -> list[Tag]:      """      Fetch tag information in static builds. +    This also includes some fake tags to preview the tag groups feature.      This will return a cached value, so it should only be used for static builds.      """ -    return fetch_tags() +    tags = fetch_tags() +    for tag in tags[3:5]: +        tag.group = "very-cool-group" +    return tags  def fetch_tags() -> list[Tag]: @@ -79,10 +83,15 @@ def fetch_tags() -> list[Tag]:              repo.extractall(folder, included)          for tag_file in Path(folder).rglob("*.md"): +            group = None +            if tag_file.parent.name != "tags": +                # Tags in sub-folders are considered part of a group +                group = tag_file.parent.name +              tags.append(Tag(                  name=tag_file.name.removesuffix(".md"), +                group=group,                  body=tag_file.read_text(encoding="utf-8"), -                url=f"{TAG_URL_BASE}/{tag_file.name}"              ))      return tags @@ -114,31 +123,85 @@ def get_tags() -> list[Tag]:          return Tag.objects.all() -def get_tag(name: str) -> Tag: -    """Return a tag by name.""" -    tags = get_tags() -    for tag in tags: -        if tag.name == name: +def get_tag(path: str) -> typing.Union[Tag, list[Tag]]: +    """ +    Return a tag based on the search location. + +    The tag name and group must match. If only one argument is provided in the path, +    it's assumed to either be a group name, or a no-group tag name. + +    If it's a group name, a list of tags which belong to it is returned. +    """ +    path = path.split("/") +    if len(path) == 2: +        group, name = path[0], path[1] +    else: +        name = path[0] +        group = None + +    matches = [] +    for tag in get_tags(): +        if tag.name == name and tag.group == group:              return tag +        elif tag.group == name and group is None: +            matches.append(tag) + +    if matches: +        return matches      raise Tag.DoesNotExist() -def get_category_pages(path: Path) -> dict[str, dict]: -    """Get all page names and their metadata at a category path.""" -    # Special handling for tags -    if path == Path(__file__).parent / "resources/tags": -        tags = {} -        for tag in get_tags(): -            content = frontmatter.parse(tag.body)[1] +def get_tag_category( +    tags: typing.Optional[list[Tag]] = None, *, collapse_groups: bool +) -> dict[str, dict]: +    """ +    Generate context data for `tags`, or all tags if None. + +    If `tags` is None, `get_tag` is used to populate the data. +    If `collapse_groups` is True, tags with parent groups are not included in the list, +    and instead the parent itself is included as a single entry with it's sub-tags +    in the description. +    """ +    if not tags: +        tags = get_tags() + +    data = [] +    groups = {} -            tags[tag.name] = { +    # Create all the metadata for the tags +    for tag in tags: +        if tag.group is None or not collapse_groups: +            content = frontmatter.parse(tag.body)[1] +            data.append({                  "title": tag.name,                  "description": markdown.markdown(content, extensions=["pymdownx.superfences"]), -                "icon": "fas fa-tag" -            } +                "icon": "fas fa-tag", +            }) +        else: +            if tag.group not in groups: +                groups[tag.group] = { +                    "title": tag.group, +                    "description": [tag.name], +                    "icon": "fas fa-tags", +                } +            else: +                groups[tag.group]["description"].append(tag.name) -        return {name: tags[name] for name in sorted(tags)} +    # Flatten group description into a single string +    for group in groups.values(): +        group["description"] = "Contains the following tags: " + ", ".join(group["description"]) +        data.append(group) + +    # Sort the tags, and return them in the proper format +    return {tag["title"]: tag for tag in sorted(data, key=lambda tag: tag["title"].lower())} + + +def get_category_pages(path: Path) -> dict[str, dict]: +    """Get all page names and their metadata at a category path.""" +    # Special handling for tags +    if path == Path(__file__).parent / "resources/tags": +        return get_tag_category(collapse_groups=True)      pages = {} diff --git a/pydis_site/apps/content/views/page_category.py b/pydis_site/apps/content/views/page_category.py index 01ce8402..062c2bc1 100644 --- a/pydis_site/apps/content/views/page_category.py +++ b/pydis_site/apps/content/views/page_category.py @@ -5,7 +5,7 @@ from django.conf import settings  from django.http import Http404, HttpRequest, HttpResponse  from django.views.generic import TemplateView -from pydis_site.apps.content import utils +from pydis_site.apps.content import models, utils  class PageOrCategoryView(TemplateView): @@ -91,4 +91,7 @@ class PageOrCategoryView(TemplateView):              "page_title": category["title"],              "page_description": category["description"],              "icon": category.get("icon"), +            "app_name": "content:page_category", +            "is_tag_listing": "/resources/tags" in path.as_posix(), +            "tag_url": models.Tag.URL_BASE,          } diff --git a/pydis_site/apps/content/views/tags.py b/pydis_site/apps/content/views/tags.py index 5295537d..a8df65db 100644 --- a/pydis_site/apps/content/views/tags.py +++ b/pydis_site/apps/content/views/tags.py @@ -1,4 +1,5 @@  import re +import typing  import frontmatter  import markdown @@ -16,23 +17,65 @@ COMMAND_REGEX = re.compile(r"`*!tags? (?P<name>[\w\d-]+)`*")  class TagView(TemplateView):      """Handles tag pages.""" -    template_name = "content/tag.html" +    tag: typing.Union[Tag, list[Tag]] +    is_group: bool + +    def setup(self, *args, **kwargs) -> None: +        """Look for a tag, and configure the view.""" +        super().setup(*args, **kwargs) -    def get_context_data(self, **kwargs) -> dict: -        """Get the relevant context for this tag page."""          try: -            tag = utils.get_tag(kwargs.get("name")) +            self.tag = utils.get_tag(kwargs.get("location")) +            self.is_group = isinstance(self.tag, list)          except Tag.DoesNotExist:              raise Http404 +    def get_template_names(self) -> list[str]: +        """Either return the tag page template, or the listing.""" +        if self.is_group: +            template_name = "content/listing.html" +        else: +            template_name = "content/tag.html" + +        return [template_name] + +    def get_context_data(self, **kwargs) -> dict: +        """Get the relevant context for this tag page or group."""          context = super().get_context_data(**kwargs) -        context["page_title"] = tag.name +        context["breadcrumb_items"] = [{ +            "name": utils.get_category(settings.CONTENT_PAGES_PATH / location)["title"], +            "path": location, +        } for location in (".", "tags")] + +        if self.is_group: +            self._set_group_context(context, self.tag) +        else: +            self._set_tag_context(context, self.tag) + +        return context + +    @staticmethod +    def _set_tag_context(context: dict[str, any], tag: Tag) -> None: +        """Update the context with the information for a tag page.""" +        context.update({ +            "page_title": tag.name, +            "tag": tag, +        }) + +        if tag.group: +            # Add group names to the breadcrumbs +            context["breadcrumb_items"].append({ +                "name": tag.group, +                "path": f"tags/{tag.group}", +            }) + +        # Clean up tag body          body = frontmatter.parse(tag.body)          content = body[1]          # Check for tags which can be hyperlinked          def sub(match: re.Match) -> str: -            link = reverse("content:tag", kwargs={"name": match.group("name")}) +            link = reverse("content:tag", kwargs={"location": match.group("name")})              return f"[{match.group()}]({link})"          content = COMMAND_REGEX.sub(sub, content) @@ -42,14 +85,20 @@ class TagView(TemplateView):              if image := embed.get("image"):                  content = f"![{embed['title']}]({image['url']})\n\n" + content +        # Insert the content +        context["page"] = markdown.markdown(content, extensions=["pymdownx.superfences"]) + +    @staticmethod +    def _set_group_context(context: dict[str, any], tags: list[Tag]) -> None: +        """Update the context with the information for a group of tags.""" +        group = tags[0].group          context.update({ -            "page": markdown.markdown(content, extensions=["pymdownx.superfences"]), -            "tag": tag, +            "categories": {}, +            "pages": utils.get_tag_category(tags, collapse_groups=False), +            "page_title": group, +            "icon": "fab fa-tags", +            "is_tag_listing": True, +            "app_name": "content:tag", +            "path": f"{group}/", +            "tag_url": f"{tags[0].URL_BASE}/{group}"          }) - -        context["breadcrumb_items"] = [{ -            "name": utils.get_category(settings.CONTENT_PAGES_PATH / location)["title"], -            "path": str(location) -        } for location in [".", "tags"]] - -        return context diff --git a/pydis_site/static/css/content/color.css b/pydis_site/static/css/content/color.css new file mode 100644 index 00000000..f4801c28 --- /dev/null +++ b/pydis_site/static/css/content/color.css @@ -0,0 +1,7 @@ +.content .fa-github { +    color: black; +} + +.content .fa-github:hover { +    color: #7289DA; +} diff --git a/pydis_site/static/css/content/tag.css b/pydis_site/static/css/content/tag.css index a3db046c..ec45bfc7 100644 --- a/pydis_site/static/css/content/tag.css +++ b/pydis_site/static/css/content/tag.css @@ -1,11 +1,3 @@ -h1.title a { -    color: black; -} - -h1.title a:hover { -    color: #7289DA; -} -  .content a * {      /* This is the original color, but propagated down the chain */      /* which allows for elements inside links, such as codeblocks */ diff --git a/pydis_site/static/js/content/listing.js b/pydis_site/static/js/content/listing.js index 3502cb2a..4b722632 100644 --- a/pydis_site/static/js/content/listing.js +++ b/pydis_site/static/js/content/listing.js @@ -4,6 +4,11 @@  function trimTag() {      const containers = document.getElementsByClassName("tag-container");      for (const container of containers) { +        if (container.textContent.startsWith("Contains the following tags:")) { +            // Tag group, no need to trim +            continue; +        } +          // Remove every element after the first two paragraphs          while (container.children.length > 2) {              container.removeChild(container.lastChild); diff --git a/pydis_site/templates/content/listing.html b/pydis_site/templates/content/listing.html index 098f4237..934b95f6 100644 --- a/pydis_site/templates/content/listing.html +++ b/pydis_site/templates/content/listing.html @@ -2,6 +2,19 @@  {% extends 'content/base.html' %}  {% load static %} +{# Show a GitHub button on tag pages #} +{% block title_element %} +{% if is_tag_listing %} +    <link rel="stylesheet" href="{% static "css/content/color.css" %}"> +    <div class="level"> +        <div class="level-left">{{ block.super }}</div> +        <div class="level-right"> +            <a class="level-item fab fa-github" href="{{ tag_url }}"></a> +        </div> +    </div> +{% endif %} +{% endblock %} +  {% block page_content %}      {# Nested Categories #}      {% for category, data in categories.items %} @@ -23,10 +36,10 @@              <span class="icon is-size-4 is-medium">                  <i class="{{ data.icon|default:"fab fa-python" }} is-size-3 is-black has-icon-padding" aria-hidden="true"></i>              </span> -            <a href="{% url "content:page_category" location=path|add:page %}"> +            <a href="{% url app_name location=path|add:page %}">                  <span class="is-size-4 has-text-weight-bold">{{ data.title }}</span>              </a> -            {% if "tags" in location %} +            {% if is_tag_listing %}                  <div class="tag-container">{{ data.description | safe }}</div>              {% else %}                  <p class="is-italic">{{ data.description }}</p> diff --git a/pydis_site/templates/content/tag.html b/pydis_site/templates/content/tag.html index 264f63d0..9bd65744 100644 --- a/pydis_site/templates/content/tag.html +++ b/pydis_site/templates/content/tag.html @@ -3,6 +3,7 @@  {% block head %}      {{ block.super }} +    <link rel="stylesheet" href="{% static 'css/content/color.css' %}"/>      <link rel="stylesheet" href="{% static 'css/content/tag.css' %}"/>      <title>{{ tag.name }}</title>  {% endblock %} @@ -15,7 +16,3 @@          </div>      </div>  {% endblock %} - -{% block page_content %} -    {{ block.super }} -{% endblock %} | 
