From a908b9aa3a18222f296c0e4bd67d815f48ada5af Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Wed, 13 Dec 2023 09:07:08 +0100 Subject: Migrate mailing lists to their own API endpoints Add a new model for the bot to store its mailing list state in, as opposed to the current JSON blob in the BotSetting table. Migrate the existing settings from the BotSetting table into the new model. --- .../apps/api/migrations/0093_add_mailing_lists.py | 36 ++++++++ .../migrations/0094_migrate_mailing_listdata.py | 41 +++++++++ pydis_site/apps/api/models/__init__.py | 2 + pydis_site/apps/api/models/bot/__init__.py | 2 + pydis_site/apps/api/models/bot/mailing_list.py | 13 +++ .../apps/api/models/bot/mailing_list_seen_item.py | 29 +++++++ pydis_site/apps/api/serializers.py | 36 ++++++++ pydis_site/apps/api/tests/test_mailing_list.py | 93 +++++++++++++++++++++ pydis_site/apps/api/urls.py | 5 ++ pydis_site/apps/api/viewsets/__init__.py | 7 +- pydis_site/apps/api/viewsets/bot/__init__.py | 10 +-- pydis_site/apps/api/viewsets/bot/mailing_list.py | 97 ++++++++++++++++++++++ 12 files changed, 362 insertions(+), 9 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0093_add_mailing_lists.py create mode 100644 pydis_site/apps/api/migrations/0094_migrate_mailing_listdata.py create mode 100644 pydis_site/apps/api/models/bot/mailing_list.py create mode 100644 pydis_site/apps/api/models/bot/mailing_list_seen_item.py create mode 100644 pydis_site/apps/api/tests/test_mailing_list.py create mode 100644 pydis_site/apps/api/viewsets/bot/mailing_list.py (limited to 'pydis_site') 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 87fd6190..a2dc68f0 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, @@ -741,3 +743,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, @@ -67,6 +68,10 @@ bot_router.register( 'infractions', 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/ + Retrieve a single mailing list and its seen items. + + #### Response format + >>> { + ... 'id': 1, + ... 'name': 'python-dev', + ... 'seen_items': [ + ... 'd81gg90290la8', + ... ... + ... ] + ... } + + ### POST /bot/mailing-lists//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) -- cgit v1.2.3 From 5cd802be1da397c448898c6aab1c3bebc2e417e3 Mon Sep 17 00:00:00 2001 From: Xithrius Date: Thu, 1 Feb 2024 23:09:49 -0800 Subject: Appeased the linter --- pydis_site/apps/content/utils.py | 2 +- pyproject.toml | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) (limited to 'pydis_site') 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/pyproject.toml b/pyproject.toml index 334ad4e4..32acf064 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,9 @@ build-backend = "poetry.core.masonry.api" [tool.ruff] target-version = "py311" extend-exclude = [".cache"] +line-length = 120 + +[tool.ruff.lint] ignore = [ "ANN002", "ANN003", "ANN101", "ANN102", "ANN204", "ANN206", "ANN401", "B904", @@ -57,10 +60,9 @@ ignore = [ "S311", "SIM102", "SIM108", ] -line-length = 120 select = ["ANN", "B", "C4", "D", "DJ", "DTZ", "E", "F", "ISC", "INT", "N", "PGH", "PIE", "RET", "RSE", "RUF", "S", "SIM", "T20", "TID", "UP", "W"] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "pydis_site/apps/**/migrations/*.py" = ["ALL"] "manage.py" = ["T201"] "pydis_site/apps/api/tests/base.py" = ["S106"] -- cgit v1.2.3 From 6cea8884316f3b3343cf7df6525358d2805520ac Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Sun, 11 Feb 2024 12:26:39 +0530 Subject: Move resorce loading from view init() to AppConfig ready() method This is done to load the resources only once on startup instead of loading it everytime the view is initialized which is done whenever a request is made to the resources endpoint. --- pydis_site/apps/resources/apps.py | 92 +++++++++++++++++++++++++++++++++ pydis_site/apps/resources/views.py | 103 ++++--------------------------------- 2 files changed, 101 insertions(+), 94 deletions(-) (limited to 'pydis_site') 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/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, } ) -- cgit v1.2.3 From 459d11329a617c885ec4c3b32822baacd1cd04c0 Mon Sep 17 00:00:00 2001 From: RohanJnr Date: Sun, 11 Feb 2024 12:28:01 +0530 Subject: Update path variable in resource page tests --- pydis_site/apps/resources/tests/test_resource_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') 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): -- cgit v1.2.3