diff options
author | 2024-02-14 15:58:13 +0800 | |
---|---|---|
committer | 2024-02-14 15:58:13 +0800 | |
commit | cc1eb08b282b37c9855843fbdc7129e40a881c11 (patch) | |
tree | 4b42f796d50e40099a21f63efdfa831ca0bacada /pydis_site | |
parent | Events: Update the events to include in galleries (diff) | |
parent | Bump sentry-sdk from 1.40.2 to 1.40.3 (#1229) (diff) |
Merge branch 'main' into feat/events-redesign
Diffstat (limited to 'pydis_site')
-rw-r--r-- | pydis_site/apps/api/migrations/0093_add_mailing_lists.py | 36 | ||||
-rw-r--r-- | pydis_site/apps/api/migrations/0094_migrate_mailing_listdata.py | 41 | ||||
-rw-r--r-- | pydis_site/apps/api/models/__init__.py | 2 | ||||
-rw-r--r-- | pydis_site/apps/api/models/bot/__init__.py | 2 | ||||
-rw-r--r-- | pydis_site/apps/api/models/bot/mailing_list.py | 13 | ||||
-rw-r--r-- | pydis_site/apps/api/models/bot/mailing_list_seen_item.py | 29 | ||||
-rw-r--r-- | pydis_site/apps/api/serializers.py | 36 | ||||
-rw-r--r-- | pydis_site/apps/api/tests/test_mailing_list.py | 93 | ||||
-rw-r--r-- | pydis_site/apps/api/urls.py | 5 | ||||
-rw-r--r-- | pydis_site/apps/api/viewsets/__init__.py | 7 | ||||
-rw-r--r-- | pydis_site/apps/api/viewsets/bot/__init__.py | 10 | ||||
-rw-r--r-- | pydis_site/apps/api/viewsets/bot/mailing_list.py | 97 | ||||
-rw-r--r-- | pydis_site/apps/content/utils.py | 2 | ||||
-rw-r--r-- | pydis_site/apps/resources/apps.py | 92 | ||||
-rw-r--r-- | pydis_site/apps/resources/tests/test_resource_data.py | 2 | ||||
-rw-r--r-- | pydis_site/apps/resources/views.py | 103 |
16 files changed, 465 insertions, 105 deletions
diff --git a/pydis_site/apps/api/migrations/0093_add_mailing_lists.py b/pydis_site/apps/api/migrations/0093_add_mailing_lists.py new file mode 100644 index 00000000..9f210b94 --- /dev/null +++ b/pydis_site/apps/api/migrations/0093_add_mailing_lists.py @@ -0,0 +1,36 @@ +# Generated by Django 5.0 on 2023-12-17 13:31 + +import django.db.models.deletion +import pydis_site.apps.api.models.mixins +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0092_remove_redirect_filter_list'), + ] + + operations = [ + migrations.CreateModel( + name='MailingList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='A short identifier for the mailing list.', max_length=50, unique=True)), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.CreateModel( + name='MailingListSeenItem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('hash', models.CharField(help_text='A hash, or similar identifier, of the content that was seen.', max_length=100)), + ('list', models.ForeignKey(help_text='The mailing list from which this seen item originates.', on_delete=django.db.models.deletion.CASCADE, related_name='seen_items', to='api.mailinglist')), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='mailinglistseenitem', + constraint=models.UniqueConstraint(fields=('list', 'hash'), name='unique_list_and_hash'), + ), + ] diff --git a/pydis_site/apps/api/migrations/0094_migrate_mailing_listdata.py b/pydis_site/apps/api/migrations/0094_migrate_mailing_listdata.py new file mode 100644 index 00000000..50598025 --- /dev/null +++ b/pydis_site/apps/api/migrations/0094_migrate_mailing_listdata.py @@ -0,0 +1,41 @@ +# Generated by Django 5.0 on 2023-12-13 07:03 + +from django.db import migrations + + +def migrate_mailing_lists_into_new_model(apps, schema_editor): + """Move the bot's mailing list information from the BotSetting to the new MailingList model.""" + + BotSetting = apps.get_model('api', 'BotSetting') + MailingList = apps.get_model('api', 'MailingList') + MailingListSeenItem = apps.get_model('api', 'MailingListSeenItem') + try: + setting = BotSetting.objects.get(name='news') + except BotSetting.DoesNotExist: + return + + # Field format: + # { + # "pep": [ + # "644", + # "8102", + # ... + for list_name, item_hashes in setting.data.items(): + (mailing_list, _created) = MailingList.objects.get_or_create(name=list_name) + MailingListSeenItem.objects.bulk_create( + MailingListSeenItem(list=mailing_list, hash=item_hash) + for item_hash in item_hashes + ) + + setting.delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0093_add_mailing_lists'), + ] + + operations = [ + migrations.RunPython(migrate_mailing_lists_into_new_model, elidable=True) + ] diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index fee4c8d5..5901c978 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -7,6 +7,8 @@ from .bot import ( DocumentationLink, DeletedMessage, Infraction, + MailingList, + MailingListSeenItem, Message, MessageDeletionContext, Nomination, diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index 6f09473d..c07a3238 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -8,6 +8,8 @@ from .infraction import Infraction from .message import Message from .aoc_completionist_block import AocCompletionistBlock from .aoc_link import AocAccountLink +from .mailing_list import MailingList +from .mailing_list_seen_item import MailingListSeenItem from .message_deletion_context import MessageDeletionContext from .nomination import Nomination, NominationEntry from .off_topic_channel_name import OffTopicChannelName diff --git a/pydis_site/apps/api/models/bot/mailing_list.py b/pydis_site/apps/api/models/bot/mailing_list.py new file mode 100644 index 00000000..eaca8fb5 --- /dev/null +++ b/pydis_site/apps/api/models/bot/mailing_list.py @@ -0,0 +1,13 @@ +from django.db import models + +from pydis_site.apps.api.models.mixins import ModelReprMixin + + +class MailingList(ModelReprMixin, models.Model): + """A mailing list that the bot is following.""" + + name = models.CharField( + max_length=50, + help_text="A short identifier for the mailing list.", + unique=True + ) diff --git a/pydis_site/apps/api/models/bot/mailing_list_seen_item.py b/pydis_site/apps/api/models/bot/mailing_list_seen_item.py new file mode 100644 index 00000000..d91cfbe6 --- /dev/null +++ b/pydis_site/apps/api/models/bot/mailing_list_seen_item.py @@ -0,0 +1,29 @@ +from django.db import models + +from pydis_site.apps.api.models.mixins import ModelReprMixin +from .mailing_list import MailingList + + +class MailingListSeenItem(ModelReprMixin, models.Model): + """An item in a mailing list that the bot has consumed and mirrored elsewhere.""" + + list = models.ForeignKey( + MailingList, + on_delete=models.CASCADE, + related_name='seen_items', + help_text="The mailing list from which this seen item originates." + ) + hash = models.CharField( + max_length=100, + help_text="A hash, or similar identifier, of the content that was seen." + ) + + class Meta: + """Prevent adding the same hash to the same list multiple times.""" + + constraints = ( + models.UniqueConstraint( + fields=('list', 'hash'), + name='unique_list_and_hash', + ), + ) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index cfd975c9..ea94214f 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -26,6 +26,8 @@ from .models import ( Filter, FilterList, Infraction, + MailingList, + MailingListSeenItem, MessageDeletionContext, Nomination, NominationEntry, @@ -733,3 +735,37 @@ class OffensiveMessageSerializer(FrozenFieldsMixin, ModelSerializer): model = OffensiveMessage fields = ('id', 'channel_id', 'delete_date') frozen_fields = ('id', 'channel_id') + + +class MailingListSeenItemListSerializer(ListSerializer): + """A class providing (de-)serialization of `MailingListSeenItem` instances as a list.""" + + def to_representation(self, objects: list[MailingListSeenItem]) -> list[str]: + """Return the hashes of each seen mailing list item.""" + return [obj['hash'] for obj in objects.values('hash')] + + +class MailingListSeenItemSerializer(ModelSerializer): + """A class providing (de-)serialization of `MailingListSeenItem` instances.""" + + class Meta: + """Metadata defined for the Django REST Framework.""" + + model = MailingListSeenItem + # Since this is only exposed on the parent mailing list model, + # we don't need information about the list or even the ID. + fields = ('hash',) + list_serializer_class = MailingListSeenItemListSerializer + + +class MailingListSerializer(FrozenFieldsMixin, ModelSerializer): + """A class providing (de-)serialization of `MailingList` instances.""" + + seen_items = MailingListSeenItemSerializer(many=True, required=False) + + class Meta: + """Metadata defined for the Django REST Framework.""" + + model = MailingList + fields = ('id', 'name', 'seen_items') + frozen_fields = ('name',) diff --git a/pydis_site/apps/api/tests/test_mailing_list.py b/pydis_site/apps/api/tests/test_mailing_list.py new file mode 100644 index 00000000..2d3025be --- /dev/null +++ b/pydis_site/apps/api/tests/test_mailing_list.py @@ -0,0 +1,93 @@ +from django.urls import reverse + +from .base import AuthenticatedAPITestCase +from pydis_site.apps.api.models import MailingList, MailingListSeenItem + + +class NoMailingListTests(AuthenticatedAPITestCase): + def test_create_mailing_list(self): + url = reverse('api:bot:mailinglist-list') + data = {'name': 'lemon-dev'} + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 201) + +class EmptyMailingListTests(AuthenticatedAPITestCase): + @classmethod + def setUpTestData(cls): + cls.list = MailingList.objects.create(name='erlang-dev') + + def test_create_duplicate_mailing_list(self): + url = reverse('api:bot:mailinglist-list') + data = {'name': self.list.name} + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 400) + + def test_get_all_mailing_lists(self): + url = reverse('api:bot:mailinglist-list') + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), [ + {'id': self.list.id, 'name': self.list.name, 'seen_items': []} + ]) + + def test_get_single_mailing_list(self): + url = reverse('api:bot:mailinglist-detail', args=(self.list.name,)) + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), { + 'id': self.list.id, 'name': self.list.name, 'seen_items': [] + }) + + def test_add_seen_item_to_mailing_list(self): + data = 'PEP-123' + url = reverse('api:bot:mailinglist-seen-items', args=(self.list.name,)) + response = self.client.post(url, data=data) + + self.assertEqual(response.status_code, 204) + self.list.refresh_from_db() + self.assertEqual(self.list.seen_items.first().hash, data) + + def test_invalid_request_body(self): + data = [ + "Dinoman, such tiny hands", + "He couldn't even ride a bike", + "He couldn't even dance", + "With the girl that he liked", + "He lived in tiny villages", + "And prayed to tiny god", + "He couldn't go to gameshow", + "Cause he could not applaud...", + ] + url = reverse('api:bot:mailinglist-seen-items', args=(self.list.name,)) + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json(), { + 'non_field_errors': ["The request body must be a string"] + }) + + +class MailingListWithSeenItemsTests(AuthenticatedAPITestCase): + @classmethod + def setUpTestData(cls): + cls.list = MailingList.objects.create(name='erlang-dev') + cls.seen_item = MailingListSeenItem.objects.create(hash='12345', list=cls.list) + + def test_get_mailing_list(self): + url = reverse('api:bot:mailinglist-detail', args=(self.list.name,)) + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), { + 'id': self.list.id, 'name': self.list.name, 'seen_items': [self.seen_item.hash] + }) + + def test_prevents_duplicate_addition_of_seen_item(self): + url = reverse('api:bot:mailinglist-seen-items', args=(self.list.name,)) + response = self.client.post(url, data=self.seen_item.hash) + + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json(), { + 'non_field_errors': ["Seen item already known."] + }) diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index 80d4edc2..5cda033a 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -17,6 +17,7 @@ from .viewsets import ( FilterListViewSet, FilterViewSet, InfractionViewSet, + MailingListViewSet, NominationViewSet, OffTopicChannelNameViewSet, OffensiveMessageViewSet, @@ -68,6 +69,10 @@ bot_router.register( InfractionViewSet ) bot_router.register( + 'mailing-lists', + MailingListViewSet +) +bot_router.register( 'nominations', NominationViewSet ) diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py index 1dae9be1..a28fa8e3 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -1,17 +1,18 @@ # flake8: noqa from .bot import ( + AocAccountLinkViewSet, + AocCompletionistBlockViewSet, BotSettingViewSet, BumpedThreadViewSet, DeletedMessageViewSet, DocumentationLinkViewSet, FilterListViewSet, - InfractionViewSet, FilterListViewSet, FilterViewSet, + InfractionViewSet, + MailingListViewSet, NominationViewSet, OffensiveMessageViewSet, - AocAccountLinkViewSet, - AocCompletionistBlockViewSet, OffTopicChannelNameViewSet, ReminderViewSet, RoleViewSet, diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index 33b65009..bb26cb11 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -1,18 +1,16 @@ # flake8: noqa -from .filters import ( - FilterListViewSet, - FilterViewSet -) +from .aoc_completionist_block import AocCompletionistBlockViewSet +from .aoc_link import AocAccountLinkViewSet from .bot_setting import BotSettingViewSet from .bumped_thread import BumpedThreadViewSet from .deleted_message import DeletedMessageViewSet from .documentation_link import DocumentationLinkViewSet +from .filters import FilterListViewSet, FilterViewSet from .infraction import InfractionViewSet +from .mailing_list import MailingListViewSet from .nomination import NominationViewSet from .off_topic_channel_name import OffTopicChannelNameViewSet from .offensive_message import OffensiveMessageViewSet -from .aoc_link import AocAccountLinkViewSet -from .aoc_completionist_block import AocCompletionistBlockViewSet from .reminder import ReminderViewSet from .role import RoleViewSet from .user import UserViewSet diff --git a/pydis_site/apps/api/viewsets/bot/mailing_list.py b/pydis_site/apps/api/viewsets/bot/mailing_list.py new file mode 100644 index 00000000..e46dfd4c --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/mailing_list.py @@ -0,0 +1,97 @@ +from django.db import IntegrityError +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.exceptions import ParseError +from rest_framework.mixins import CreateModelMixin, ListModelMixin, RetrieveModelMixin +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from pydis_site.apps.api.serializers import MailingListSerializer +from pydis_site.apps.api.models import MailingList, MailingListSeenItem + + +class MailingListViewSet(GenericViewSet, CreateModelMixin, ListModelMixin, RetrieveModelMixin): + """ + View providing management and updates of mailing lists and their seen items. + + ## Routes + + ### GET /bot/mailing-lists + Returns all the mailing lists and their seen items. + + #### Response format + >>> [ + ... { + ... 'id': 1, + ... 'name': 'python-dev', + ... 'seen_items': [ + ... 'd81gg90290la8', + ... ... + ... ] + ... }, + ... ... + ... ] + + ### POST /bot/mailing-lists + Create a new mailing list. + + #### Request format + >>> { + ... 'name': str + ... } + + #### Status codes + - 201: when the mailing list was created successfully + - 400: if the request data was invalid + + ### GET /bot/mailing-lists/<name:str> + Retrieve a single mailing list and its seen items. + + #### Response format + >>> { + ... 'id': 1, + ... 'name': 'python-dev', + ... 'seen_items': [ + ... 'd81gg90290la8', + ... ... + ... ] + ... } + + ### POST /bot/mailing-lists/<name:str>/seen-items + Add a single seen item to the given mailing list. The request body should + be the hash of the seen item to add, as a plain string. + + #### Request body + >>> str + + #### Response format + Empty response. + + #### Status codes + - 204: on successful creation of the seen item + - 400: if the request data was invalid + - 404: when the mailing list with the given name could not be found + """ + + lookup_field = 'name' + serializer_class = MailingListSerializer + queryset = MailingList.objects.prefetch_related('seen_items') + + @action(detail=True, methods=["POST"], + name="Add a seen item for a mailing list", url_name='seen-items', url_path='seen-items') + def add_seen_item(self, request: Request, name: str) -> Response: + """Add a single seen item to the given mailing list.""" + if not isinstance(request.data, str): + raise ParseError(detail={'non_field_errors': ["The request body must be a string"]}) + + list_ = self.get_object() + seen_item = MailingListSeenItem(list=list_, hash=request.data) + try: + seen_item.save() + except IntegrityError as err: + if err.__cause__.diag.constraint_name == 'unique_list_and_hash': + raise ParseError(detail={'non_field_errors': ["Seen item already known."]}) + raise # pragma: no cover + + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/pydis_site/apps/content/utils.py b/pydis_site/apps/content/utils.py index 9c949a93..5a146e10 100644 --- a/pydis_site/apps/content/utils.py +++ b/pydis_site/apps/content/utils.py @@ -107,7 +107,7 @@ def fetch_tags() -> list[Tag]: for file in repo.getmembers(): if "/bot/resources/tags" in file.path: included.append(file) - repo.extractall(folder, included) + repo.extractall(folder, included) # noqa: S202 for tag_file in Path(folder).rglob("*.md"): name = tag_file.name diff --git a/pydis_site/apps/resources/apps.py b/pydis_site/apps/resources/apps.py index 93117654..51cb064b 100644 --- a/pydis_site/apps/resources/apps.py +++ b/pydis_site/apps/resources/apps.py @@ -1,7 +1,99 @@ +from pathlib import Path + +import yaml from django.apps import AppConfig +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 ResourcesConfig(AppConfig): """AppConfig instance for Resources app.""" name = 'pydis_site.apps.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 ", "the_")): + return name[4:] + return name + + + def ready(self) -> None: + """Set up all the resources.""" + # 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: + # 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"]], + } diff --git a/pydis_site/apps/resources/tests/test_resource_data.py b/pydis_site/apps/resources/tests/test_resource_data.py index 3a96e8b9..d96d840e 100644 --- a/pydis_site/apps/resources/tests/test_resource_data.py +++ b/pydis_site/apps/resources/tests/test_resource_data.py @@ -1,7 +1,7 @@ import yaml from django.test import TestCase -from pydis_site.apps.resources.views import RESOURCES_PATH +from pydis_site.apps.resources.apps import RESOURCES_PATH class TestResourceData(TestCase): diff --git a/pydis_site/apps/resources/views.py b/pydis_site/apps/resources/views.py index a2cd8d0c..3632b2e2 100644 --- a/pydis_site/apps/resources/views.py +++ b/pydis_site/apps/resources/views.py @@ -1,114 +1,29 @@ import json -from pathlib import Path -import yaml +from django.apps import apps 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") +APP_NAME = "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 ", "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: - # 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: str | None = 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. + + app = apps.get_app_config(APP_NAME) + if resource_type: dashless_resource_type = resource_type.replace("-", " ") - if dashless_resource_type.title() not in self.filters["Type"]["filters"]: + if dashless_resource_type.title() not in app.filters["Type"]["filters"]: return HttpResponseNotFound() resource_type = resource_type.replace(" ", "-") @@ -117,9 +32,9 @@ class ResourceView(View): request, template_name="resources/resources.html", context={ - "resources": self.resources, - "filters": self.filters, - "valid_filters": json.dumps(self.valid_filters), + "resources": app.resources, + "filters": app.filters, + "valid_filters": json.dumps(app.valid_filters), "resource_type": resource_type, } ) |