aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Rohan Reddy Alleti <[email protected]>2021-06-10 02:32:50 +0530
committerGravatar GitHub <[email protected]>2021-06-10 02:32:50 +0530
commitc444b82e20bda8b3ba27ca31ecab3135edc4f289 (patch)
tree8933c7928e19b8923d5c78e8022e7df3666e9650
parentUse assertListEqual where applicable instead of assertEqual. (diff)
parentMerge pull request #524 from python-discord/swfarnsworth/resource_suggestion_... (diff)
Merge branch 'main' into otn_softdel
-rw-r--r--.github/ISSUE_TEMPLATE/resource_suggestion.md21
-rw-r--r--docs/setup.md4
-rw-r--r--pydis_site/apps/api/tests/test_infractions.py179
-rw-r--r--pydis_site/apps/api/viewsets/bot/infraction.py73
-rw-r--r--pydis_site/apps/content/resources/frequently-asked-questions.md2
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/cloning-repository.md23
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/supplemental-information.md2
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/installing-project-dependencies.md32
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md11
-rw-r--r--pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md2
-rw-r--r--pydis_site/apps/content/tests/helpers.py50
-rw-r--r--pydis_site/apps/content/tests/test_utils.py31
-rw-r--r--pydis_site/apps/content/tests/test_views.py11
-rw-r--r--pydis_site/apps/home/views/home.py4
-rw-r--r--pydis_site/constants.py1
15 files changed, 363 insertions, 83 deletions
diff --git a/.github/ISSUE_TEMPLATE/resource_suggestion.md b/.github/ISSUE_TEMPLATE/resource_suggestion.md
new file mode 100644
index 00000000..f09f8a9b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/resource_suggestion.md
@@ -0,0 +1,21 @@
+---
+name: Resource Suggestion
+about: Suggest a resource for the Python Discord resource index.
+title: 'Resource Suggestion: '
+labels: 'resource suggestion'
+assignees: 'swfarnsworth'
+---
+
+**Resource name**
+
+**Resource location**\
+Should be a link of some kind, either to the resource itself or (in the case of resources that must be purchased) an information page about it.
+
+**Payment type**\
+Options are free, paid, and subscription. Combinations of these are allowed for special cases (like a limited free version).
+
+**Why it should be included**\
+A brief explanation for why you think this resource is valuable.
+
+**Potential limitations**\
+Is the resource easy to use? Does it contain good information but have poorly-written code? Is it outdated in some way? If so, explain why it should still be included.
diff --git a/docs/setup.md b/docs/setup.md
index d992067e..d88021cc 100644
--- a/docs/setup.md
+++ b/docs/setup.md
@@ -82,8 +82,8 @@ Make sure you add the following to your hosts file:
```
When trying to access the site, you'll be using the domains above instead of the usual `localhost:8000`.
-Finally, you will need to set the environment variable `DEBUG=1`. When using `pipenv`, you can
-set put this into an `.env` file to have it exported automatically. It's also recommended to
+Finally, you will need to set the environment variable `DEBUG=1`. If you have `python-dotenv` installed, you can put this into a
+ `.env` file to have it exported automatically. It's also recommended to
export `LOG_LEVEL=INFO` when using `DEBUG=1` if you don't want super verbose logs.
To run the server, run `python manage.py runserver`. If it gives you an error saying
diff --git a/pydis_site/apps/api/tests/test_infractions.py b/pydis_site/apps/api/tests/test_infractions.py
index 82b497aa..9aae16c0 100644
--- a/pydis_site/apps/api/tests/test_infractions.py
+++ b/pydis_site/apps/api/tests/test_infractions.py
@@ -1,3 +1,4 @@
+import datetime
from datetime import datetime as dt, timedelta, timezone
from unittest.mock import patch
from urllib.parse import quote
@@ -16,7 +17,7 @@ class UnauthenticatedTests(APISubdomainTestCase):
self.client.force_authenticate(user=None)
def test_detail_lookup_returns_401(self):
- url = reverse('bot:infraction-detail', args=(5,), host='api')
+ url = reverse('bot:infraction-detail', args=(6,), host='api')
response = self.client.get(url)
self.assertEqual(response.status_code, 401)
@@ -34,7 +35,7 @@ class UnauthenticatedTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 401)
def test_partial_update_returns_401(self):
- url = reverse('bot:infraction-detail', args=(5,), host='api')
+ url = reverse('bot:infraction-detail', args=(6,), host='api')
response = self.client.patch(url, data={'reason': 'Have a nice day.'})
self.assertEqual(response.status_code, 401)
@@ -44,7 +45,7 @@ class InfractionTests(APISubdomainTestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create(
- id=5,
+ id=6,
name='james',
discriminator=1,
)
@@ -64,6 +65,30 @@ class InfractionTests(APISubdomainTestCase):
reason='James is an ass, and we won\'t be working with him again.',
active=False
)
+ cls.mute_permanent = Infraction.objects.create(
+ user_id=cls.user.id,
+ actor_id=cls.user.id,
+ type='mute',
+ reason='He has a filthy mouth and I am his soap.',
+ active=True,
+ expires_at=None
+ )
+ cls.superstar_expires_soon = Infraction.objects.create(
+ user_id=cls.user.id,
+ actor_id=cls.user.id,
+ type='superstar',
+ reason='This one doesn\'t matter anymore.',
+ active=True,
+ expires_at=datetime.datetime.utcnow() + datetime.timedelta(hours=5)
+ )
+ cls.voiceban_expires_later = Infraction.objects.create(
+ user_id=cls.user.id,
+ actor_id=cls.user.id,
+ type='voice_ban',
+ reason='Jet engine mic',
+ active=True,
+ expires_at=datetime.datetime.utcnow() + datetime.timedelta(days=5)
+ )
def test_list_all(self):
"""Tests the list-view, which should be ordered by inserted_at (newest first)."""
@@ -73,9 +98,12 @@ class InfractionTests(APISubdomainTestCase):
self.assertEqual(response.status_code, 200)
infractions = response.json()
- self.assertEqual(len(infractions), 2)
- self.assertEqual(infractions[0]['id'], self.ban_inactive.id)
- self.assertEqual(infractions[1]['id'], self.ban_hidden.id)
+ self.assertEqual(len(infractions), 5)
+ self.assertEqual(infractions[0]['id'], self.voiceban_expires_later.id)
+ self.assertEqual(infractions[1]['id'], self.superstar_expires_soon.id)
+ self.assertEqual(infractions[2]['id'], self.mute_permanent.id)
+ self.assertEqual(infractions[3]['id'], self.ban_inactive.id)
+ self.assertEqual(infractions[4]['id'], self.ban_hidden.id)
def test_filter_search(self):
url = reverse('bot:infraction-list', host='api')
@@ -98,6 +126,140 @@ class InfractionTests(APISubdomainTestCase):
self.assertEqual(len(infractions), 1)
self.assertEqual(infractions[0]['id'], self.ban_hidden.id)
+ def test_filter_permanent_false(self):
+ url = reverse('bot:infraction-list', host='api')
+ response = self.client.get(f'{url}?type=mute&permanent=false')
+
+ self.assertEqual(response.status_code, 200)
+ infractions = response.json()
+
+ self.assertEqual(len(infractions), 0)
+
+ def test_filter_permanent_true(self):
+ url = reverse('bot:infraction-list', host='api')
+ response = self.client.get(f'{url}?type=mute&permanent=true')
+
+ self.assertEqual(response.status_code, 200)
+ infractions = response.json()
+
+ self.assertEqual(infractions[0]['id'], self.mute_permanent.id)
+
+ def test_filter_after(self):
+ url = reverse('bot:infraction-list', host='api')
+ target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5)
+ response = self.client.get(f'{url}?type=superstar&expires_after={target_time.isoformat()}')
+
+ self.assertEqual(response.status_code, 200)
+ infractions = response.json()
+ self.assertEqual(len(infractions), 0)
+
+ def test_filter_before(self):
+ url = reverse('bot:infraction-list', host='api')
+ target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5)
+ response = self.client.get(f'{url}?type=superstar&expires_before={target_time.isoformat()}')
+
+ self.assertEqual(response.status_code, 200)
+ infractions = response.json()
+ self.assertEqual(len(infractions), 1)
+ self.assertEqual(infractions[0]['id'], self.superstar_expires_soon.id)
+
+ def test_filter_after_invalid(self):
+ url = reverse('bot:infraction-list', host='api')
+ response = self.client.get(f'{url}?expires_after=gibberish')
+
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(list(response.json())[0], "expires_after")
+
+ def test_filter_before_invalid(self):
+ url = reverse('bot:infraction-list', host='api')
+ response = self.client.get(f'{url}?expires_before=000000000')
+
+ self.assertEqual(response.status_code, 400)
+ self.assertEqual(list(response.json())[0], "expires_before")
+
+ def test_after_before_before(self):
+ url = reverse('bot:infraction-list', host='api')
+ target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=4)
+ target_time_late = datetime.datetime.utcnow() + datetime.timedelta(hours=6)
+ response = self.client.get(
+ f'{url}?expires_before={target_time_late.isoformat()}'
+ f'&expires_after={target_time.isoformat()}'
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(response.json()), 1)
+ self.assertEqual(response.json()[0]["id"], self.superstar_expires_soon.id)
+
+ def test_after_after_before_invalid(self):
+ url = reverse('bot:infraction-list', host='api')
+ target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5)
+ target_time_late = datetime.datetime.utcnow() + datetime.timedelta(hours=9)
+ response = self.client.get(
+ f'{url}?expires_before={target_time.isoformat()}'
+ f'&expires_after={target_time_late.isoformat()}'
+ )
+
+ self.assertEqual(response.status_code, 400)
+ errors = list(response.json())
+ self.assertIn("expires_before", errors)
+ self.assertIn("expires_after", errors)
+
+ def test_permanent_after_invalid(self):
+ url = reverse('bot:infraction-list', host='api')
+ target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5)
+ response = self.client.get(f'{url}?permanent=true&expires_after={target_time.isoformat()}')
+
+ self.assertEqual(response.status_code, 400)
+ errors = list(response.json())
+ self.assertEqual("permanent", errors[0])
+
+ def test_permanent_before_invalid(self):
+ url = reverse('bot:infraction-list', host='api')
+ target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=5)
+ response = self.client.get(f'{url}?permanent=true&expires_before={target_time.isoformat()}')
+
+ self.assertEqual(response.status_code, 400)
+ errors = list(response.json())
+ self.assertEqual("permanent", errors[0])
+
+ def test_nonpermanent_before(self):
+ url = reverse('bot:infraction-list', host='api')
+ target_time = datetime.datetime.utcnow() + datetime.timedelta(hours=6)
+ response = self.client.get(
+ f'{url}?permanent=false&expires_before={target_time.isoformat()}'
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(len(response.json()), 1)
+ self.assertEqual(response.json()[0]["id"], self.superstar_expires_soon.id)
+
+ def test_filter_manytypes(self):
+ url = reverse('bot:infraction-list', host='api')
+ response = self.client.get(f'{url}?types=mute,ban')
+
+ self.assertEqual(response.status_code, 200)
+ infractions = response.json()
+ self.assertEqual(len(infractions), 3)
+
+ def test_types_type_invalid(self):
+ url = reverse('bot:infraction-list', host='api')
+ response = self.client.get(f'{url}?types=mute,ban&type=superstar')
+
+ self.assertEqual(response.status_code, 400)
+ errors = list(response.json())
+ self.assertEqual("types", errors[0])
+
+ def test_sort_expiresby(self):
+ url = reverse('bot:infraction-list', host='api')
+ response = self.client.get(f'{url}?ordering=expires_at&permanent=false')
+ self.assertEqual(response.status_code, 200)
+ infractions = response.json()
+
+ self.assertEqual(len(infractions), 3)
+ self.assertEqual(infractions[0]['id'], self.superstar_expires_soon.id)
+ self.assertEqual(infractions[1]['id'], self.voiceban_expires_later.id)
+ self.assertEqual(infractions[2]['id'], self.ban_hidden.id)
+
def test_returns_empty_for_no_match(self):
url = reverse('bot:infraction-list', host='api')
response = self.client.get(f'{url}?type=ban&search=poop')
@@ -502,7 +664,10 @@ class CreationTests(APISubdomainTestCase):
)
def test_integrity_error_if_missing_active_field(self):
- pattern = 'null value in column "active" violates not-null constraint'
+ pattern = (
+ 'null value in column "active" (of relation "api_infraction" )?'
+ 'violates not-null constraint'
+ )
with self.assertRaisesRegex(IntegrityError, pattern):
Infraction.objects.create(
user=self.user,
diff --git a/pydis_site/apps/api/viewsets/bot/infraction.py b/pydis_site/apps/api/viewsets/bot/infraction.py
index bd512ddd..f8b0cb9d 100644
--- a/pydis_site/apps/api/viewsets/bot/infraction.py
+++ b/pydis_site/apps/api/viewsets/bot/infraction.py
@@ -1,3 +1,6 @@
+from datetime import datetime
+
+from django.db.models import QuerySet
from django.http.request import HttpRequest
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.decorators import action
@@ -43,10 +46,17 @@ class InfractionViewSet(
- **offset** `int`: the initial index from which to return the results (default 0)
- **search** `str`: regular expression applied to the infraction's reason
- **type** `str`: the type of the infraction
+ - **types** `str`: comma separated sequence of types to filter for
- **user__id** `int`: snowflake of the user to which the infraction was applied
- **ordering** `str`: comma-separated sequence of fields to order the returned results
+ - **permanent** `bool`: whether or not to retrieve permanent infractions (default True)
+ - **expires_after** `isodatetime`: the earliest expires_at time to return infractions for
+ - **expires_before** `isodatetime`: the latest expires_at time to return infractions for
Invalid query parameters are ignored.
+ Only one of `type` and `types` may be provided. If both `expires_before` and `expires_after`
+ are provided, `expires_after` must come after `expires_before`.
+ If `permanent` is provided and true, `expires_before` and `expires_after` must not be provided.
#### Response format
Response is paginated but the result is returned without any pagination metadata.
@@ -156,6 +166,69 @@ class InfractionViewSet(
return Response(serializer.data)
+ def get_queryset(self) -> QuerySet:
+ """
+ Called to fetch the initial queryset, used to implement some of the more complex filters.
+
+ This provides the `permanent` and the `expires_gte` and `expires_lte` options.
+ """
+ filter_permanent = self.request.query_params.get('permanent')
+ additional_filters = {}
+ if filter_permanent is not None:
+ additional_filters['expires_at__isnull'] = filter_permanent.lower() == 'true'
+
+ filter_expires_after = self.request.query_params.get('expires_after')
+ if filter_expires_after:
+ try:
+ additional_filters['expires_at__gte'] = datetime.fromisoformat(
+ filter_expires_after
+ )
+ except ValueError:
+ raise ValidationError({'expires_after': ['failed to convert to datetime']})
+
+ filter_expires_before = self.request.query_params.get('expires_before')
+ if filter_expires_before:
+ try:
+ additional_filters['expires_at__lte'] = datetime.fromisoformat(
+ filter_expires_before
+ )
+ except ValueError:
+ raise ValidationError({'expires_before': ['failed to convert to datetime']})
+
+ if 'expires_at__lte' in additional_filters and 'expires_at__gte' in additional_filters:
+ if additional_filters['expires_at__gte'] > additional_filters['expires_at__lte']:
+ raise ValidationError({
+ 'expires_before': ['cannot be after expires_after'],
+ 'expires_after': ['cannot be before expires_before'],
+ })
+
+ if (
+ ('expires_at__lte' in additional_filters or 'expires_at__gte' in additional_filters)
+ and 'expires_at__isnull' in additional_filters
+ and additional_filters['expires_at__isnull']
+ ):
+ raise ValidationError({
+ 'permanent': [
+ 'cannot filter for permanent infractions at the'
+ ' same time as expires_at or expires_before',
+ ]
+ })
+
+ if filter_expires_before:
+ # Filter out permanent infractions specifically if we want ones that will expire
+ # before a given date
+ additional_filters['expires_at__isnull'] = False
+
+ filter_types = self.request.query_params.get('types')
+ if filter_types:
+ if self.request.query_params.get('type'):
+ raise ValidationError({
+ 'types': ['you must provide only one of "type" or "types"'],
+ })
+ additional_filters['type__in'] = [i.strip() for i in filter_types.split(",")]
+
+ return self.queryset.filter(**additional_filters)
+
@action(url_path='expanded', detail=False)
def list_expanded(self, *args, **kwargs) -> Response:
"""
diff --git a/pydis_site/apps/content/resources/frequently-asked-questions.md b/pydis_site/apps/content/resources/frequently-asked-questions.md
index 8b9945aa..212ea5f8 100644
--- a/pydis_site/apps/content/resources/frequently-asked-questions.md
+++ b/pydis_site/apps/content/resources/frequently-asked-questions.md
@@ -87,6 +87,8 @@ The only file types that we allow on this server are those that Discord supports
This is because it's easier and safer for people on the server since they do not need to download a file to view it.
It's also to ease the burden on our moderators, otherwise they would have to download and check the files posted to the server.
+Even though Discord does support previewing of files like `.txt` and `.py`, that support is only available on Desktop, not mobile. Additionally, we prefer people to use hastebin as it encourages them to only copy over the relevant code snippets instead of their whole code; this makes helping much easier for all involved.
+
If you want to share code please use our hosted hastebin, [paste.pythondiscord.com](http://paste.pythondiscord.com).
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/cloning-repository.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/cloning-repository.md
index fad54374..23d525b8 100644
--- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/cloning-repository.md
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/cloning-repository.md
@@ -6,26 +6,29 @@ icon: fab fa-github
> **Note:** The process varies depending on your choice of code editor / IDE, so refer to one of the following guides:
-- [Cloning with PyCharm](#cloning-with-pycharm)
- [Cloning with the command line](#cloning-with-the-command-line)
+- [Cloning with PyCharm](#cloning-with-pycharm)
The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories. You should have already retrieved your fork's Git URL as described in [**Creating a Fork**](../forking-repository).
---
-## Cloning with PyCharm
-1. Load up PyCharm and click `Get from VCS`.<br>
-![Create Project in PyCharm](/static/images/content/contributing/pycharm_create_project.png)
-2. Enter the URL of your forked repository.
-3. Change the directory if you desire and click `Clone`.<br>
-![Clone Git Project in Pycharm](/static/images/content/contributing/pycharm_checkout.png)
-
----
-
## Cloning with the command line
+
1. Clone your forked repository using `git clone` followed by your fork's Git URL. Then, change your working directory to the repository.
+
```shell
$ git clone https://github.com/<your username>/sir-lancebot
...
$ cd sir-lancebot
```
+
+---
+
+## Cloning with PyCharm
+
+1. Load up PyCharm and click `Get from VCS`.<br>
+ ![Create Project in PyCharm](/static/images/content/contributing/pycharm_create_project.png)
+2. Enter the URL of your forked repository.
+3. Change the directory if you desire and click `Clone`.<br>
+ ![Clone Git Project in Pycharm](/static/images/content/contributing/pycharm_checkout.png)
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/supplemental-information.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/supplemental-information.md
index 24dc9aa9..70d47563 100644
--- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/supplemental-information.md
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/contributing-guidelines/supplemental-information.md
@@ -40,7 +40,7 @@ If the linter complains, the commit is aborted so that you can fix the linting e
That way, you never commit the problematic code in the first place!
Please refer to the project-specific documentation to see how to setup and run those tools.
-In most cases, you can install pre-commit using `pipenv run precommit` or `poetry run task precommit`, and lint using `pipenv run lint` or `poetry run task lint`.
+In most cases, you can install pre-commit using `poetry run task precommit`, and lint using `poetry run task lint`.
## Type Hinting
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/installing-project-dependencies.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/installing-project-dependencies.md
index 653b71aa..26d6de30 100644
--- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/installing-project-dependencies.md
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/installing-project-dependencies.md
@@ -6,34 +6,36 @@ icon: fab fa-python
> **Note:** The process varies depending on your choice of code editor / IDE, so refer to one of the following guides:
-- [Installing dependencies with PyCharm](#installing-dependencies-with-pycharm)
- [Installing dependencies with the command line](#installing-dependencies-with-the-command-line)
+- [Installing dependencies with PyCharm](#installing-dependencies-with-pycharm)
The following will use the [Sir-Lancebot](https://github.com/python-discord/sir-lancebot/) repository as an example, but the steps are the same for all other repositories.
You should have already cloned your fork as described in [**Cloning a Repository**](../cloning-repository).
---
-## Installing dependencies with PyCharm
-1. Load up your project in PyCharm.
-2. Go to the Project Settings by clicking `File`, then `Settings...`. Alternatively, use the shortcut key: `Ctrl+Alt+S` (`command+comma` on Mac OS).
-3. Install the [poetry plugin](https://plugins.jetbrains.com/plugin/14307-poetry).
-4. Navigate to `Project Interpreter`, then click the gear icon and click `Add`.<br/>
-![PyCharm Interpreter Settings](/static/images/content/contributing/pycharm_interpreter.png)<br/>
-5. Click `Poetry Environment`, then click `OK`.<br/>
-![PyCharm Pipenv Environment](/static/images/content/contributing/pycharm_poetry.png)<br/>
-6. PyCharm will automatically install the packages required into a virtual environment.<br/>
-![PyCharm Project Interpreter](/static/images/content/contributing/pycharm_poetry_success.png)
-
----
-
## Installing dependencies with the command line
+
1. Make sure you are in the root project directory. This directory will always have a file titled `README.md`.
2. Install project and development dependencies. Remember to also set up pre-commit hooks to ensure your pushed commits will never fail linting.
----
+---
```shell
$ poetry install
$ poetry run task precommit
```
+
+---
+
+## Installing dependencies with PyCharm
+
+1. Load up your project in PyCharm.
+2. Go to the Project Settings by clicking `File`, then `Settings...`. Alternatively, use the shortcut key: `Ctrl+Alt+S` (`command+comma` on Mac OS).
+3. Install the [poetry plugin](https://plugins.jetbrains.com/plugin/14307-poetry).
+4. Navigate to `Project Interpreter`, then click the gear icon and click `Add`.<br/>
+ ![PyCharm Interpreter Settings](/static/images/content/contributing/pycharm_interpreter.png)<br/>
+5. Click `Poetry Environment`, then click `OK`.<br/>
+ ![PyCharm Poetry Environment](/static/images/content/contributing/pycharm_poetry.png)<br/>
+6. PyCharm will automatically install the packages required into a virtual environment.<br/>
+ ![PyCharm Project Interpreter](/static/images/content/contributing/pycharm_poetry_success.png)
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md
index 75d27d99..ada47931 100644
--- a/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/contributing/site.md
@@ -7,9 +7,9 @@ toc: 1
# Requirements
-- [Python 3.8](https://www.python.org/downloads/)
-- [Pipenv](https://github.com/pypa/pipenv#installation)
- - `pip install pipenv`
+- [Python 3.9](https://www.python.org/downloads/)
+- [Poetry](https://python-poetry.org/docs/#installation)
+ - `pip install poetry`
- [Git](https://git-scm.com/downloads)
- [Windows](https://git-scm.com/download/win)
- [MacOS](https://git-scm.com/download/mac) or `brew install git`
@@ -62,6 +62,7 @@ Run the following queries to create the user and database:
```sql
CREATE USER pysite WITH SUPERUSER PASSWORD 'pysite';
CREATE DATABASE pysite WITH OWNER pysite;
+CREATE DATABASE metricity WITH OWNER pysite;
```
Finally, enter `/q` to exit psql.
@@ -122,10 +123,10 @@ If you're not using Docker, then use [pg_ctl](https://www.postgresql.org/docs/cu
### Webserver
-Starting the webserver is done simply through pipenv:
+Starting the webserver is done simply through poetry:
```shell
-pipenv run start
+poetry run task start
```
---
diff --git a/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md b/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md
index 2a6e7781..8b7c5584 100644
--- a/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md
+++ b/pydis_site/apps/content/resources/guides/pydis-guides/help-channel-guide.md
@@ -32,7 +32,7 @@ If you're not sure where to post, feel free to ask us which channel is relevant
Our general help channels move at a fast pace, and attract a far more diverse spectrum of helpers.
This is a great choice for a generic Python question, and a good choice if you need an answer as soon as possible.
-It's particularly important to [ask good questions](..guides/asking-good-questions) when asking in these channels, or you risk not getting an answer and having your help channel be claimed by someone else.
+It's particularly important to [ask good questions](../asking-good-questions) when asking in these channels, or you risk not getting an answer and having your help channel be claimed by someone else.
## How To Claim a Channel
diff --git a/pydis_site/apps/content/tests/helpers.py b/pydis_site/apps/content/tests/helpers.py
index 29140375..d897c024 100644
--- a/pydis_site/apps/content/tests/helpers.py
+++ b/pydis_site/apps/content/tests/helpers.py
@@ -1,4 +1,13 @@
-from pyfakefs.fake_filesystem_unittest import TestCase
+from pathlib import Path
+
+from pyfakefs import fake_filesystem_unittest
+
+
+# Set the module constant within Patcher to use the fake filesystem
+# https://jmcgeheeiv.github.io/pyfakefs/master/usage.html#modules-to-reload
+with fake_filesystem_unittest.Patcher() as _:
+ BASE_PATH = Path("res")
+
# Valid markdown content with YAML metadata
MARKDOWN_WITH_METADATA = """
@@ -41,11 +50,11 @@ PARSED_METADATA = {
PARSED_CATEGORY_INFO = {"title": "Category Name", "description": "Description"}
-class MockPagesTestCase(TestCase):
+class MockPagesTestCase(fake_filesystem_unittest.TestCase):
"""
TestCase with a fake filesystem for testing.
- Structure:
+ Structure (relative to BASE_PATH):
├── _info.yml
├── root.md
├── root_without_metadata.md
@@ -68,24 +77,27 @@ class MockPagesTestCase(TestCase):
"""Create the fake filesystem."""
self.setUpPyfakefs()
- self.fs.create_file("_info.yml", contents=CATEGORY_INFO)
- self.fs.create_file("root.md", contents=MARKDOWN_WITH_METADATA)
- self.fs.create_file("root_without_metadata.md", contents=MARKDOWN_WITHOUT_METADATA)
- self.fs.create_file("not_a_page.md/_info.yml", contents=CATEGORY_INFO)
- self.fs.create_file("category/_info.yml", contents=CATEGORY_INFO)
- self.fs.create_file("category/with_metadata.md", contents=MARKDOWN_WITH_METADATA)
- self.fs.create_file("category/subcategory/_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_file(f"{BASE_PATH}/_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_file(f"{BASE_PATH}/root.md", contents=MARKDOWN_WITH_METADATA)
+ self.fs.create_file(
+ f"{BASE_PATH}/root_without_metadata.md", contents=MARKDOWN_WITHOUT_METADATA
+ )
+ self.fs.create_file(f"{BASE_PATH}/not_a_page.md/_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_file(f"{BASE_PATH}/category/_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_file(
+ f"{BASE_PATH}/category/with_metadata.md", contents=MARKDOWN_WITH_METADATA
+ )
+ self.fs.create_file(f"{BASE_PATH}/category/subcategory/_info.yml", contents=CATEGORY_INFO)
self.fs.create_file(
- "category/subcategory/with_metadata.md", contents=MARKDOWN_WITH_METADATA
+ f"{BASE_PATH}/category/subcategory/with_metadata.md", contents=MARKDOWN_WITH_METADATA
)
self.fs.create_file(
- "category/subcategory/without_metadata.md", contents=MARKDOWN_WITHOUT_METADATA
+ f"{BASE_PATH}/category/subcategory/without_metadata.md",
+ contents=MARKDOWN_WITHOUT_METADATA
)
- # There is always a `tmp` directory in the filesystem, so make it a category
- # for testing purposes.
- # See: https://jmcgeheeiv.github.io/pyfakefs/release/usage.html#os-temporary-directories
- self.fs.create_file("tmp/_info.yml", contents=CATEGORY_INFO)
- self.fs.create_file("tmp.md", contents=MARKDOWN_WITH_METADATA)
- self.fs.create_file("tmp/category/_info.yml", contents=CATEGORY_INFO)
- self.fs.create_dir("tmp/category/subcategory_without_info")
+ temp = f"{BASE_PATH}/tmp" # noqa: S108
+ self.fs.create_file(f"{temp}/_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_file(f"{temp}.md", contents=MARKDOWN_WITH_METADATA)
+ self.fs.create_file(f"{temp}/category/_info.yml", contents=CATEGORY_INFO)
+ self.fs.create_dir(f"{temp}/category/subcategory_without_info")
diff --git a/pydis_site/apps/content/tests/test_utils.py b/pydis_site/apps/content/tests/test_utils.py
index 6612e44c..be5ea897 100644
--- a/pydis_site/apps/content/tests/test_utils.py
+++ b/pydis_site/apps/content/tests/test_utils.py
@@ -4,7 +4,7 @@ from django.http import Http404
from pydis_site.apps.content import utils
from pydis_site.apps.content.tests.helpers import (
- MockPagesTestCase, PARSED_CATEGORY_INFO, PARSED_HTML, PARSED_METADATA
+ BASE_PATH, MockPagesTestCase, PARSED_CATEGORY_INFO, PARSED_HTML, PARSED_METADATA
)
@@ -12,41 +12,46 @@ class GetCategoryTests(MockPagesTestCase):
"""Tests for the get_category function."""
def test_get_valid_category(self):
- result = utils.get_category(Path("category"))
+ result = utils.get_category(Path(BASE_PATH, "category"))
self.assertEqual(result, {"title": "Category Name", "description": "Description"})
def test_get_nonexistent_category(self):
with self.assertRaises(Http404):
- utils.get_category(Path("invalid"))
+ utils.get_category(Path(BASE_PATH, "invalid"))
def test_get_category_with_path_to_file(self):
# Valid categories are directories, not files
with self.assertRaises(Http404):
- utils.get_category(Path("root.md"))
+ utils.get_category(Path(BASE_PATH, "root.md"))
def test_get_category_without_info_yml(self):
# Categories should provide an _info.yml file
with self.assertRaises(FileNotFoundError):
- utils.get_category(Path("tmp/category/subcategory_without_info"))
+ utils.get_category(Path(BASE_PATH, "tmp/category/subcategory_without_info"))
class GetCategoriesTests(MockPagesTestCase):
"""Tests for the get_categories function."""
def test_get_root_categories(self):
- result = utils.get_categories(Path("."))
+ result = utils.get_categories(BASE_PATH)
info = PARSED_CATEGORY_INFO
- self.assertEqual(result, {"category": info, "tmp": info, "not_a_page.md": info})
+ categories = {
+ "category": info,
+ "tmp": info,
+ "not_a_page.md": info,
+ }
+ self.assertEqual(result, categories)
def test_get_categories_with_subcategories(self):
- result = utils.get_categories(Path("category"))
+ result = utils.get_categories(Path(BASE_PATH, "category"))
self.assertEqual(result, {"subcategory": PARSED_CATEGORY_INFO})
def test_get_categories_without_subcategories(self):
- result = utils.get_categories(Path("category/subcategory"))
+ result = utils.get_categories(Path(BASE_PATH, "category/subcategory"))
self.assertEqual(result, {})
@@ -56,14 +61,14 @@ class GetCategoryPagesTests(MockPagesTestCase):
def test_get_pages_in_root_category_successfully(self):
"""The method should successfully retrieve page metadata."""
- root_category_pages = utils.get_category_pages(Path("."))
+ root_category_pages = utils.get_category_pages(BASE_PATH)
self.assertEqual(
root_category_pages, {"root": PARSED_METADATA, "root_without_metadata": {}}
)
def test_get_pages_in_subcategories_successfully(self):
"""The method should successfully retrieve page metadata."""
- category_pages = utils.get_category_pages(Path("category"))
+ category_pages = utils.get_category_pages(Path(BASE_PATH, "category"))
# Page metadata is properly retrieved
self.assertEqual(category_pages, {"with_metadata": PARSED_METADATA})
@@ -84,10 +89,10 @@ class GetPageTests(MockPagesTestCase):
for msg, page_path, expected_html, expected_metadata in cases:
with self.subTest(msg=msg):
- html, metadata = utils.get_page(Path(page_path))
+ html, metadata = utils.get_page(Path(BASE_PATH, page_path))
self.assertEqual(html, expected_html)
self.assertEqual(metadata, expected_metadata)
def test_get_nonexistent_page_returns_404(self):
with self.assertRaises(Http404):
- utils.get_page(Path("invalid"))
+ utils.get_page(Path(BASE_PATH, "invalid"))
diff --git a/pydis_site/apps/content/tests/test_views.py b/pydis_site/apps/content/tests/test_views.py
index 74d38f78..b6e752d6 100644
--- a/pydis_site/apps/content/tests/test_views.py
+++ b/pydis_site/apps/content/tests/test_views.py
@@ -3,27 +3,20 @@ from unittest import TestCase
from django.http import Http404
from django.test import RequestFactory, SimpleTestCase, override_settings
-from pyfakefs import fake_filesystem_unittest
from pydis_site.apps.content.tests.helpers import (
- MockPagesTestCase, PARSED_CATEGORY_INFO, PARSED_HTML, PARSED_METADATA
+ BASE_PATH, MockPagesTestCase, PARSED_CATEGORY_INFO, PARSED_HTML, PARSED_METADATA
)
from pydis_site.apps.content.views import PageOrCategoryView
-# Set the module constant within Patcher to use the fake filesystem
-# https://jmcgeheeiv.github.io/pyfakefs/master/usage.html#modules-to-reload
-with fake_filesystem_unittest.Patcher() as _:
- BASE_PATH = Path(".")
-
-
def patch_dispatch_attributes(view: PageOrCategoryView, location: str) -> None:
"""
Set the attributes set in the `dispatch` method manually.
This is necessary because it is never automatically called during tests.
"""
- view.location = Path(location)
+ view.location = Path(BASE_PATH, location)
# URL location on the filesystem
view.full_location = view.location
diff --git a/pydis_site/apps/home/views/home.py b/pydis_site/apps/home/views/home.py
index e77772fb..b3767d37 100644
--- a/pydis_site/apps/home/views/home.py
+++ b/pydis_site/apps/home/views/home.py
@@ -9,6 +9,7 @@ from django.utils import timezone
from django.views import View
from pydis_site.apps.home.models import RepositoryMetadata
+from pydis_site.constants import GITHUB_TOKEN
log = logging.getLogger(__name__)
@@ -18,6 +19,7 @@ class HomeView(View):
github_api = "https://api.github.com/users/python-discord/repos?per_page=100"
repository_cache_ttl = 3600
+ headers = {"Authorization": f"token {GITHUB_TOKEN}"}
# Which of our GitHub repos should be displayed on the front page, and in which order?
repos = [
@@ -42,7 +44,7 @@ class HomeView(View):
repo_dict = {}
# Fetch the data from the GitHub API
- api_data: List[dict] = requests.get(self.github_api).json()
+ api_data: List[dict] = requests.get(self.github_api, headers=self.headers).json()
# Process the API data into our dict
for repo in api_data:
diff --git a/pydis_site/constants.py b/pydis_site/constants.py
index c7ab5db0..e6a63d12 100644
--- a/pydis_site/constants.py
+++ b/pydis_site/constants.py
@@ -1,3 +1,4 @@
import os
GIT_SHA = os.environ.get("GIT_SHA", "development")
+GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")