From 0524176aa3392aa9978a420c6089012e91ebbbc3 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Fri, 14 Apr 2023 14:42:16 +0200 Subject: Add README to the resources app (#934) --- pydis_site/apps/resources/README.md | 29 ++++++ pydis_site/apps/resources/urls.py | 8 +- pydis_site/apps/resources/views.py | 126 +++++++++++++++++++++++++++ pydis_site/apps/resources/views/__init__.py | 3 - pydis_site/apps/resources/views/resources.py | 126 --------------------------- 5 files changed, 160 insertions(+), 132 deletions(-) create mode 100644 pydis_site/apps/resources/README.md create mode 100644 pydis_site/apps/resources/views.py delete mode 100644 pydis_site/apps/resources/views/__init__.py delete mode 100644 pydis_site/apps/resources/views/resources.py (limited to 'pydis_site/apps/resources') diff --git a/pydis_site/apps/resources/README.md b/pydis_site/apps/resources/README.md new file mode 100644 index 00000000..6f41319a --- /dev/null +++ b/pydis_site/apps/resources/README.md @@ -0,0 +1,29 @@ +# The "resources" app + +This Django application powering the resources list [on our +website](https://www.pythondiscord.com/resources/). + +## Directory structure + +The main point of interest here lies in the `resources` directory: every +`.yaml` file in here represents a resource that is listed on our website. If +you are looking for the place to suggest new resources, said directory is the +place to create a new YAML file. In regards to the required keys and our +values, it's best to check the other files we have for a reference. + +The app has a single view in `views.py` that takes care of reading the `.yaml` +file. This is a standard Django view, mounted in `urls.py` as usual. + +Similar to the [home app](../home), the `templatetags` directory contains custom +[template tags and +filters](https://docs.djangoproject.com/en/dev/howto/custom-template-tags/) used +here. + +The `tests` directory validates that our redirects and helper functions work as +expected. If you made changes to the app and are looking for guidance on adding +new tests, the [Django tutorial introducing automated +testing](https://docs.djangoproject.com/en/dev/intro/tutorial05/) is a good +place to start. + +This application does not use the database and as such does not have models nor +migrations. diff --git a/pydis_site/apps/resources/urls.py b/pydis_site/apps/resources/urls.py index ed24dc99..cb33a9d7 100644 --- a/pydis_site/apps/resources/urls.py +++ b/pydis_site/apps/resources/urls.py @@ -1,9 +1,11 @@ from django_distill import distill_path -from pydis_site.apps.resources import views +from pydis_site.apps.resources.views import ResourceView app_name = "resources" urlpatterns = [ - distill_path("", views.resources.ResourceView.as_view(), name="index"), - distill_path("/", views.resources.ResourceView.as_view(), name="index"), + # Using `distill_path` instead of `path` allows this to be available + # in static preview builds. + distill_path("", ResourceView.as_view(), name="index"), + distill_path("/", ResourceView.as_view(), name="index"), ] diff --git a/pydis_site/apps/resources/views.py b/pydis_site/apps/resources/views.py new file mode 100644 index 00000000..2375f722 --- /dev/null +++ b/pydis_site/apps/resources/views.py @@ -0,0 +1,126 @@ +import json +import typing as t +from pathlib import Path + +import yaml +from django.core.handlers.wsgi import WSGIRequest +from django.http import HttpResponse, HttpResponseNotFound +from django.shortcuts import render +from django.views import View + +from pydis_site import settings +from pydis_site.apps.resources.templatetags.to_kebabcase import to_kebabcase + +RESOURCES_PATH = Path(settings.BASE_DIR, "pydis_site", "apps", "resources", "resources") + + +class ResourceView(View): + """Our curated list of good learning resources.""" + + @staticmethod + def _sort_key_disregard_the(tuple_: tuple) -> str: + """Sort a tuple by its key alphabetically, disregarding 'the' as a prefix.""" + name, resource = tuple_ + name = name.casefold() + if name.startswith("the ") or name.startswith("the_"): + return name[4:] + return name + + def __init__(self, *args, **kwargs): + """Set up all the resources.""" + super().__init__(*args, **kwargs) + + # Load the resources from the yaml files in /resources/ + self.resources = { + path.stem: yaml.safe_load(path.read_text()) + for path in RESOURCES_PATH.rglob("*.yaml") + } + + # Sort the resources alphabetically + self.resources = dict(sorted(self.resources.items(), key=self._sort_key_disregard_the)) + + # Parse out all current tags + resource_tags = { + "topics": set(), + "payment_tiers": set(), + "difficulty": set(), + "type": set(), + } + for resource_name, resource in self.resources.items(): + css_classes = [] + for tag_type in resource_tags.keys(): + # Store the tags into `resource_tags` + tags = resource.get("tags", {}).get(tag_type, []) + for tag in tags: + tag = tag.title() + tag = tag.replace("And", "and") + resource_tags[tag_type].add(tag) + + # Make a CSS class friendly representation too, while we're already iterating. + for tag in tags: + css_tag = to_kebabcase(f"{tag_type}-{tag}") + css_classes.append(css_tag) + + # Now add the css classes back to the resource, so we can use them in the template. + self.resources[resource_name]["css_classes"] = " ".join(css_classes) + + # Set up all the filter checkbox metadata + self.filters = { + "Difficulty": { + "filters": sorted(resource_tags.get("difficulty")), + "icon": "fas fa-brain", + "hidden": False, + }, + "Type": { + "filters": sorted(resource_tags.get("type")), + "icon": "fas fa-photo-video", + "hidden": False, + }, + "Payment tiers": { + "filters": sorted(resource_tags.get("payment_tiers")), + "icon": "fas fa-dollar-sign", + "hidden": True, + }, + "Topics": { + "filters": sorted(resource_tags.get("topics")), + "icon": "fas fa-lightbulb", + "hidden": True, + } + } + + # The bottom topic should always be "Other". + self.filters["Topics"]["filters"].remove("Other") + self.filters["Topics"]["filters"].append("Other") + + # A complete list of valid filter names + self.valid_filters = { + "topics": [to_kebabcase(topic) for topic in self.filters["Topics"]["filters"]], + "payment_tiers": [ + to_kebabcase(tier) for tier in self.filters["Payment tiers"]["filters"] + ], + "type": [to_kebabcase(type_) for type_ in self.filters["Type"]["filters"]], + "difficulty": [to_kebabcase(tier) for tier in self.filters["Difficulty"]["filters"]], + } + + def get(self, request: WSGIRequest, resource_type: t.Optional[str] = None) -> HttpResponse: + """List out all the resources, and any filtering options from the URL.""" + # Add type filtering if the request is made to somewhere like /resources/video. + # We also convert all spaces to dashes, so they'll correspond with the filters. + if resource_type: + dashless_resource_type = resource_type.replace("-", " ") + + if dashless_resource_type.title() not in self.filters["Type"]["filters"]: + return HttpResponseNotFound() + + resource_type = resource_type.replace(" ", "-") + + return render( + request, + template_name="resources/resources.html", + context={ + "resources": self.resources, + "filters": self.filters, + "valid_filters": json.dumps(self.valid_filters), + "resource_type": resource_type, + } + ) diff --git a/pydis_site/apps/resources/views/__init__.py b/pydis_site/apps/resources/views/__init__.py deleted file mode 100644 index 986f3e10..00000000 --- a/pydis_site/apps/resources/views/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .resources import ResourceView - -__all__ = ["ResourceView"] diff --git a/pydis_site/apps/resources/views/resources.py b/pydis_site/apps/resources/views/resources.py deleted file mode 100644 index 2375f722..00000000 --- a/pydis_site/apps/resources/views/resources.py +++ /dev/null @@ -1,126 +0,0 @@ -import json -import typing as t -from pathlib import Path - -import yaml -from django.core.handlers.wsgi import WSGIRequest -from django.http import HttpResponse, HttpResponseNotFound -from django.shortcuts import render -from django.views import View - -from pydis_site import settings -from pydis_site.apps.resources.templatetags.to_kebabcase import to_kebabcase - -RESOURCES_PATH = Path(settings.BASE_DIR, "pydis_site", "apps", "resources", "resources") - - -class ResourceView(View): - """Our curated list of good learning resources.""" - - @staticmethod - def _sort_key_disregard_the(tuple_: tuple) -> str: - """Sort a tuple by its key alphabetically, disregarding 'the' as a prefix.""" - name, resource = tuple_ - name = name.casefold() - if name.startswith("the ") or name.startswith("the_"): - return name[4:] - return name - - def __init__(self, *args, **kwargs): - """Set up all the resources.""" - super().__init__(*args, **kwargs) - - # Load the resources from the yaml files in /resources/ - self.resources = { - path.stem: yaml.safe_load(path.read_text()) - for path in RESOURCES_PATH.rglob("*.yaml") - } - - # Sort the resources alphabetically - self.resources = dict(sorted(self.resources.items(), key=self._sort_key_disregard_the)) - - # Parse out all current tags - resource_tags = { - "topics": set(), - "payment_tiers": set(), - "difficulty": set(), - "type": set(), - } - for resource_name, resource in self.resources.items(): - css_classes = [] - for tag_type in resource_tags.keys(): - # Store the tags into `resource_tags` - tags = resource.get("tags", {}).get(tag_type, []) - for tag in tags: - tag = tag.title() - tag = tag.replace("And", "and") - resource_tags[tag_type].add(tag) - - # Make a CSS class friendly representation too, while we're already iterating. - for tag in tags: - css_tag = to_kebabcase(f"{tag_type}-{tag}") - css_classes.append(css_tag) - - # Now add the css classes back to the resource, so we can use them in the template. - self.resources[resource_name]["css_classes"] = " ".join(css_classes) - - # Set up all the filter checkbox metadata - self.filters = { - "Difficulty": { - "filters": sorted(resource_tags.get("difficulty")), - "icon": "fas fa-brain", - "hidden": False, - }, - "Type": { - "filters": sorted(resource_tags.get("type")), - "icon": "fas fa-photo-video", - "hidden": False, - }, - "Payment tiers": { - "filters": sorted(resource_tags.get("payment_tiers")), - "icon": "fas fa-dollar-sign", - "hidden": True, - }, - "Topics": { - "filters": sorted(resource_tags.get("topics")), - "icon": "fas fa-lightbulb", - "hidden": True, - } - } - - # The bottom topic should always be "Other". - self.filters["Topics"]["filters"].remove("Other") - self.filters["Topics"]["filters"].append("Other") - - # A complete list of valid filter names - self.valid_filters = { - "topics": [to_kebabcase(topic) for topic in self.filters["Topics"]["filters"]], - "payment_tiers": [ - to_kebabcase(tier) for tier in self.filters["Payment tiers"]["filters"] - ], - "type": [to_kebabcase(type_) for type_ in self.filters["Type"]["filters"]], - "difficulty": [to_kebabcase(tier) for tier in self.filters["Difficulty"]["filters"]], - } - - def get(self, request: WSGIRequest, resource_type: t.Optional[str] = None) -> HttpResponse: - """List out all the resources, and any filtering options from the URL.""" - # Add type filtering if the request is made to somewhere like /resources/video. - # We also convert all spaces to dashes, so they'll correspond with the filters. - if resource_type: - dashless_resource_type = resource_type.replace("-", " ") - - if dashless_resource_type.title() not in self.filters["Type"]["filters"]: - return HttpResponseNotFound() - - resource_type = resource_type.replace(" ", "-") - - return render( - request, - template_name="resources/resources.html", - context={ - "resources": self.resources, - "filters": self.filters, - "valid_filters": json.dumps(self.valid_filters), - "resource_type": resource_type, - } - ) -- cgit v1.2.3