From ec2ae9d1ef38d44cfeeb4abbe3bda035afe93e2b Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Fri, 4 Oct 2019 08:13:18 +0200 Subject: Add validation rules to Infraction serializer https://github.com/python-discord/site/issues/273 This commit adds validation rules to the Infraction serializer that validate if a given infraction should be accepted based on its status of being considered `active`. If the validation fails, the API will reject the request and return a 400 status. Specifically, this validator checks that: - infractions that can never be active do not have `active=True` set; - a user can never receive a second active infraction of the same type. Tests have been added to `test_infractions.py` to ensure that the validators work as expected. This commit implements the first part of #273 --- pydis_site/apps/api/models/bot/user.py | 2 +- pydis_site/apps/api/serializers.py | 13 ++++ pydis_site/apps/api/tests/test_infractions.py | 85 +++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index 21617dc4..5140d2bf 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -50,7 +50,7 @@ class User(ModelReprMixin, models.Model): def __str__(self): """Returns the name and discriminator for the current user, for display purposes.""" - return f"{self.name}#{self.discriminator}" + return f"{self.name}#{self.discriminator:0>4}" @property def top_role(self) -> Role: diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 326e20e1..784bb614 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -1,4 +1,5 @@ """Converters from Django models to data interchange formats and back.""" +import logging from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField, ValidationError from rest_framework_bulk import BulkSerializerMixin @@ -12,6 +13,8 @@ from .models import ( Tag, User ) +log = logging.getLogger(__name__) + class BotSettingSerializer(ModelSerializer): """A class providing (de-)serialization of `BotSetting` instances.""" @@ -110,6 +113,16 @@ class InfractionSerializer(ModelSerializer): """Validate data constraints for the given data and abort if it is invalid.""" infr_type = attrs.get('type') + active = attrs.get('active') + if active and infr_type in ('note', 'warning', 'kick'): + raise ValidationError({'active': [f'{infr_type} infractions cannot be active.']}) + + user = attrs.get('user') + if active and Infraction.objects.filter(user=user, type=infr_type, active=True).exists(): + raise ValidationError( + {'active': [f'This user already has an active {infr_type} infraction']} + ) + expires_at = attrs.get('expires_at') if expires_at and infr_type in ('kick', 'warning'): raise ValidationError({'expires_at': [f'{infr_type} infractions cannot expire.']}) diff --git a/pydis_site/apps/api/tests/test_infractions.py b/pydis_site/apps/api/tests/test_infractions.py index c58c32e2..8279b6a6 100644 --- a/pydis_site/apps/api/tests/test_infractions.py +++ b/pydis_site/apps/api/tests/test_infractions.py @@ -305,6 +305,91 @@ class CreationTests(APISubdomainTestCase): 'hidden': [f'{data["type"]} infractions must be hidden.'] }) + def test_returns_400_for_active_infraction_of_type_that_cannot_be_active(self): + """Test if the API rejects active infractions for types that cannot be active.""" + url = reverse('bot:infraction-list', host='api') + restricted_types = ('note', 'warning', 'kick') + + for infraction_type in restricted_types: + with self.subTest(infraction_type=infraction_type): + invalid_infraction = { + 'user': self.user.id, + 'actor': self.user.id, + 'type': infraction_type, + 'reason': 'Take me on!', + 'hidden': True, + 'active': True, + 'expires_at': '2019-10-04T12:52:00+00:00' + } + response = self.client.post(url, data=invalid_infraction) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json(), { + 'active': [f'{infraction_type} infractions cannot be active.'] + }) + + def test_returns_400_for_second_active_infraction_of_the_same_type(self): + """Test if the API rejects a second active infraction of the same type for a given user.""" + url = reverse('bot:infraction-list', host='api') + active_infraction_types = ('mute', 'ban', 'superstar') + + for infraction_type in active_infraction_types: + with self.subTest(infraction_type=infraction_type): + first_active_infraction = { + 'user': self.user.id, + 'actor': self.user.id, + 'type': infraction_type, + 'reason': 'Take me on!', + 'active': True, + 'expires_at': '2019-10-04T12:52:00+00:00' + } + + # Post the first active infraction of a type and confirm it's accepted. + first_response = self.client.post(url, data=first_active_infraction) + self.assertEqual(first_response.status_code, 201) + + second_active_infraction = { + 'user': self.user.id, + 'actor': self.user.id, + 'type': infraction_type, + 'reason': 'Take on me!', + 'active': True, + 'expires_at': '2019-10-04T12:52:00+00:00' + } + second_response = self.client.post(url, data=second_active_infraction) + self.assertEqual(second_response.status_code, 400) + self.assertEqual(second_response.json(), { + 'active': [f'This user already has an active {infraction_type} infraction'] + }) + + def test_returns_201_for_second_active_infraction_of_different_type(self): + """Test if the API accepts a second active infraction of a different type than the first.""" + url = reverse('bot:infraction-list', host='api') + first_active_infraction = { + 'user': self.user.id, + 'actor': self.user.id, + 'type': 'mute', + 'reason': 'Be silent!', + 'hidden': True, + 'active': True, + 'expires_at': '2019-10-04T12:52:00+00:00' + } + second_active_infraction = { + 'user': self.user.id, + 'actor': self.user.id, + 'type': 'ban', + 'reason': 'Be gone!', + 'hidden': True, + 'active': True, + 'expires_at': '2019-10-05T12:52:00+00:00' + } + # Post the first active infraction of a type and confirm it's accepted. + first_response = self.client.post(url, data=first_active_infraction) + self.assertEqual(first_response.status_code, 201) + + # Post the first active infraction of a type and confirm it's accepted. + second_response = self.client.post(url, data=second_active_infraction) + self.assertEqual(second_response.status_code, 201) + class ExpandedTests(APISubdomainTestCase): @classmethod -- cgit v1.2.3 From cf1a0755580b109d5f35331b6d5cbfef0d92fa97 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Mon, 7 Oct 2019 08:59:23 +0200 Subject: Migrate undesirable active infraction to inactive https://github.com/python-discord/site/issues/273 This commit adds a data migration to migrate active infractions that should not be active to inactive. There are two types of infractions that this migration will migrate to inactive: - Infractions of types that should never be active (e.g. notes) - Secondary active infractions if a given user already has an active infraction of the same type. Since this makes the migration file fairly complex, I have written tests to make sure the migration works as expected. In order to do this, I've subclassed `django.test.TestCase` to create a `MigrationsTestCase` that takes care of reverting the database back to a state prior to the migrations we want to test and injects test data before applying the migrations we want to test. For more information, see `pydis_site.apps.api.tests.migrations.base` This implements the last part of and closes #273 --- .../0044_active_infractions_migration.py | 106 ++++ pydis_site/apps/api/tests/migrations/__init__.py | 1 + pydis_site/apps/api/tests/migrations/base.py | 102 ++++ .../migrations/test_active_infraction_migration.py | 545 +++++++++++++++++++++ 4 files changed, 754 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0044_active_infractions_migration.py create mode 100644 pydis_site/apps/api/tests/migrations/__init__.py create mode 100644 pydis_site/apps/api/tests/migrations/base.py create mode 100644 pydis_site/apps/api/tests/migrations/test_active_infraction_migration.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0044_active_infractions_migration.py b/pydis_site/apps/api/migrations/0044_active_infractions_migration.py new file mode 100644 index 00000000..14746712 --- /dev/null +++ b/pydis_site/apps/api/migrations/0044_active_infractions_migration.py @@ -0,0 +1,106 @@ +# Generated by Django 2.2.6 on 2019-10-07 15:59 + +from django.db import migrations +from django.db.models import Count, Prefetch, Q, QuerySet + + +class ExpirationWrapper: + """Wraps an expiration date to properly compare permanent and temporary infractions.""" + + def __init__(self, infraction): + self.expiration_date = infraction.expires_at + + def __lt__(self, other): + """An `expiration_date` is considered smaller when it comes earlier than the `other`.""" + if self.expiration_date is None: + # A permanent infraction can never end sooner than another infraction + return False + elif other.expiration_date is None: + # If `self` is temporary, but `other` is permanent, `self` is smaller + return True + else: + return self.expiration_date < other.expiration_date + + def __eq__(self, other): + """If both expiration dates are permanent they're equal, otherwise compare dates.""" + if self.expiration_date is None and other.expiration_date is None: + return True + elif self.expiration_date is None or other.expiration_date is None: + return False + else: + return self.expiration_date == other.expiration_date + + +def migrate_inactive_types_to_inactive(apps, schema_editor): + """Migrates infractions of non-active types to inactive.""" + inactive_infraction_types = Q(type="note") | Q(type="warning") | Q(type="kick") + infraction_model = apps.get_model('api', 'Infraction') + infraction_model.objects.filter(inactive_infraction_types).update(active=False) + + +def get_query(user_model, infraction_model, infr_type: str) -> QuerySet: + """ + Creates QuerySet to fetch users with multiple active infractions of the given `type`. + + The QuerySet will prefetch the infractions and attach them as an `.infractions` attribute to the + `User` instances. + """ + active_infractions = infraction_model.objects.filter(type=infr_type, active=True) + + # Build an SQL query by chaining methods together + + # Get users with active infraction(s) of the provided `infr_type` + query = user_model.objects.filter( + Q(infractions_received__type=infr_type, infractions_received__active=True) + ) + + # Prefetch their active received infractions of `infr_type` and attach `.infractions` attribute + query = query.prefetch_related( + Prefetch('infractions_received', queryset=active_infractions, to_attr='infractions') + ) + + # Count and only include them if they have at least 2 active infractions of the `type` + query = query.annotate(num_infractions=Count('infractions_received')) + query = query.filter(num_infractions__gte=2) + + # Make sure we return each individual only once + query = query.distinct() + + return query + + +def migrate_multiple_active_infractions_per_user_to_one(apps, schema_editor): + """ + Make sure a user only has one active infraction of a given "active" infraction type. + + If a user has multiple active infraction, we keep the one with longest expiration date active + and migrate the others to inactive. + """ + infraction_model = apps.get_model('api', 'Infraction') + user_model = apps.get_model('api', 'User') + + for infraction_type in ('ban', 'mute', 'superstar', 'watch'): + query = get_query(user_model, infraction_model, infraction_type) + for user in query: + infractions = sorted(user.infractions, key=ExpirationWrapper, reverse=True) + for infraction in infractions[1:]: + infraction.active = False + infraction.save() + + +def reverse_migration(apps, schema_editor): + """There's no need to do anything special to reverse these migrations.""" + return + + +class Migration(migrations.Migration): + """Data migration to get the database consistent with the new infraction validation rules.""" + + dependencies = [ + ('api', '0043_infraction_hidden_warnings_to_notes'), + ] + + operations = [ + migrations.RunPython(migrate_inactive_types_to_inactive, reverse_migration), + migrations.RunPython(migrate_multiple_active_infractions_per_user_to_one, reverse_migration) + ] diff --git a/pydis_site/apps/api/tests/migrations/__init__.py b/pydis_site/apps/api/tests/migrations/__init__.py new file mode 100644 index 00000000..38e42ffc --- /dev/null +++ b/pydis_site/apps/api/tests/migrations/__init__.py @@ -0,0 +1 @@ +"""This submodule contains tests for functions used in data migrations.""" diff --git a/pydis_site/apps/api/tests/migrations/base.py b/pydis_site/apps/api/tests/migrations/base.py new file mode 100644 index 00000000..0c0a5bd0 --- /dev/null +++ b/pydis_site/apps/api/tests/migrations/base.py @@ -0,0 +1,102 @@ +"""Includes utilities for testing migrations.""" +from django.db import connection +from django.db.migrations.executor import MigrationExecutor +from django.test import TestCase + + +class MigrationsTestCase(TestCase): + """ + A `TestCase` subclass to test migration files. + + To be able to properly test a migration, we will need to inject data into the test database + before the migrations we want to test are applied, but after the older migrations have been + applied. This makes sure that we are testing "as if" we were actually applying this migration + to a database in the state it was in before introducing the new migration. + + To set up a MigrationsTestCase, create a subclass of this class and set the following + class-level attributes: + + - app: The name of the app that contains the migrations (e.g., `'api'`) + - migration_prior: The name* of the last migration file before the migrations you want to test + - migration_target: The name* of the last migration file we want to test + + *) Specify the file names without a path or the `.py` file extension. + + Additionally, overwrite the `setUpMigrationData` in the subclass to inject data into the + database before the migrations we want to test are applied. Please read the docstring of the + method for more information. An optional hook, `setUpPostMigrationData` is also provided. + """ + + # These class-level attributes should be set in classes that inherit from this base class. + app = None + migration_prior = None + migration_target = None + + @classmethod + def setUpTestData(cls): + """ + Injects data into the test database prior to the migration we're trying to test. + + This class methods reverts the test database back to the state of the last migration file + prior to the migrations we want to test. It will then allow the user to inject data into the + test database by calling the `setUpMigrationData` hook. After the data has been injected, it + will apply the migrations we want to test and call the `setUpPostMigrationData` hook. The + user can now test if the migration correctly migrated the injected test data. + """ + if not cls.app: + raise ValueError("The `app` attribute was not set.") + + if not cls.migration_prior or not cls.migration_target: + raise ValueError("Both ` migration_prior` and `migration_target` need to be set.") + + cls.migrate_from = [(cls.app, cls.migration_prior)] + cls.migrate_to = [(cls.app, cls.migration_target)] + + # Reverse to database state prior to the migrations we want to test + executor = MigrationExecutor(connection) + executor.migrate(cls.migrate_from) + + # Call the data injection hook with the current state of the project + old_apps = executor.loader.project_state(cls.migrate_from).apps + cls.setUpMigrationData(old_apps) + + # Run the migrations we want to test + executor = MigrationExecutor(connection) + executor.loader.build_graph() + executor.migrate(cls.migrate_to) + + # Save the project state so we're able to work with the correct model states + cls.apps = executor.loader.project_state(cls.migrate_to).apps + + # Call `setUpPostMigrationData` to potentially set up post migration data used in testing + cls.setUpPostMigrationData(cls.apps) + + @classmethod + def setUpMigrationData(cls, apps): + """ + Override this method to inject data into the test database before the migration is applied. + + This method will be called after setting up the database according to the migrations that + come before the migration(s) we are trying to test, but before the to-be-tested migration(s) + are applied. This allows us to simulate a database state just prior to the migrations we are + trying to test. + + To make sure we're creating objects according to the state the models were in at this point + in the migration history, use `apps.get_model(app_name: str, model_name: str)` to get the + appropriate model, e.g.: + + >>> Infraction = apps.get_model('api', 'Infraction') + """ + pass + + @classmethod + def setUpPostMigrationData(cls, apps): + """ + Set up additional test data after the target migration has been applied. + + Use `apps.get_model(app_name: str, model_name: str)` to get the correct instances of the + model classes: + + >>> Infraction = apps.get_model('api', 'Infraction') + """ + pass diff --git a/pydis_site/apps/api/tests/migrations/test_active_infraction_migration.py b/pydis_site/apps/api/tests/migrations/test_active_infraction_migration.py new file mode 100644 index 00000000..cfae4219 --- /dev/null +++ b/pydis_site/apps/api/tests/migrations/test_active_infraction_migration.py @@ -0,0 +1,545 @@ +"""Tests for the data migration in `filename`.""" +import logging +from collections import ChainMap, namedtuple +from datetime import timedelta +from itertools import count +from typing import Dict, Iterable, Type, Union + +from django.db.models import Q +from django.forms.models import model_to_dict +from django.utils import timezone + +from pydis_site.apps.api.models import Infraction, User +from .base import MigrationsTestCase + +log = logging.getLogger(__name__) +log.setLevel(logging.DEBUG) + + +InfractionHistory = namedtuple('InfractionHistory', ("user_id", "infraction_history")) + + +class InfractionFactory: + """Factory that creates infractions for a User instance.""" + + infraction_id = count(1) + user_id = count(1) + default_values = { + 'active': True, + 'expires_at': None, + 'hidden': False, + } + + @classmethod + def create( + cls, + actor: User, + infractions: Iterable[Dict[str, Union[str, int, bool]]], + infraction_model: Type[Infraction] = Infraction, + user_model: Type[User] = User, + ) -> InfractionHistory: + """ + Creates `infractions` for the `user` with the given `actor`. + + The `infractions` dictionary can contain the following fields: + - `type` (required) + - `active` (default: True) + - `expires_at` (default: None; i.e, permanent) + - `hidden` (default: False). + + The parameters `infraction_model` and `user_model` can be used to pass in an instance of + both model classes from a different migration/project state. + """ + user_id = next(cls.user_id) + user = user_model.objects.create( + id=user_id, + name=f"Infracted user {user_id}", + discriminator=user_id, + avatar_hash=None, + ) + infraction_history = [] + + for infraction in infractions: + infraction = dict(infraction) + infraction["id"] = next(cls.infraction_id) + infraction = ChainMap(infraction, cls.default_values) + new_infraction = infraction_model.objects.create( + user=user, + actor=actor, + type=infraction["type"], + reason=f"`{infraction['type']}` infraction (ID: {infraction['id']} of {user}", + active=infraction['active'], + hidden=infraction['hidden'], + expires_at=infraction['expires_at'], + ) + infraction_history.append(new_infraction) + + return InfractionHistory(user_id=user_id, infraction_history=infraction_history) + + +class InfractionFactoryTests(MigrationsTestCase): + """Tests for the InfractionFactory.""" + + app = "api" + migration_prior = "0043_infraction_hidden_warnings_to_notes" + migration_target = "0043_infraction_hidden_warnings_to_notes" + + @classmethod + def setUpPostMigrationData(cls, apps): + """Create a default actor for all infractions.""" + cls.infraction_model = apps.get_model('api', 'Infraction') + cls.user_model = apps.get_model('api', 'User') + + cls.actor = cls.user_model.objects.create( + id=9999, + name="Unknown Moderator", + discriminator=1040, + avatar_hash=None, + ) + + def test_infraction_factory_total_count(self): + """Does the test database hold as many infractions as we tried to create?""" + InfractionFactory.create( + actor=self.actor, + infractions=( + {'type': 'kick', 'active': False, 'hidden': False}, + {'type': 'ban', 'active': True, 'hidden': False}, + {'type': 'note', 'active': False, 'hidden': True}, + ), + infraction_model=self.infraction_model, + user_model=self.user_model, + ) + database_count = Infraction.objects.all().count() + self.assertEqual(3, database_count) + + def test_infraction_factory_multiple_users(self): + """Does the test database hold as many infractions as we tried to create?""" + created_infractions = {} + for user in range(5): + created_infractions.update( + { + user: InfractionFactory.create( + actor=self.actor, + infractions=( + {'type': 'kick', 'active': False, 'hidden': True}, + {'type': 'ban', 'active': True, 'hidden': False}, + ), + infraction_model=self.infraction_model, + user_model=self.user_model, + ) + } + ) + + # Check if infractions and users are recorded properly in the database + database_count = Infraction.objects.all().count() + self.assertEqual(database_count, 10) + + user_count = User.objects.all().count() + self.assertEqual(user_count, 5 + 1) + + def test_infraction_factory_sets_correct_fields(self): + """Does the InfractionFactory set the correct attributes?""" + infractions = ( + { + 'type': 'note', + 'active': False, + 'hidden': True, + 'expires_at': timezone.now() + }, + {'type': 'warning', 'active': False, 'hidden': False, 'expires_at': None}, + {'type': 'watch', 'active': False, 'hidden': True, 'expires_at': None}, + {'type': 'mute', 'active': True, 'hidden': False, 'expires_at': None}, + {'type': 'kick', 'active': True, 'hidden': True, 'expires_at': None}, + {'type': 'ban', 'active': True, 'hidden': False, 'expires_at': None}, + { + 'type': 'superstar', + 'active': True, + 'hidden': True, + 'expires_at': timezone.now() + }, + ) + + InfractionFactory.create( + actor=self.actor, + infractions=infractions, + infraction_model=self.infraction_model, + user_model=self.user_model, + ) + + for infraction in infractions: + with self.subTest(**infraction): + self.assertTrue(Infraction.objects.filter(**infraction).exists()) + + +class ActiveInfractionMigrationTests(MigrationsTestCase): + """ + Tests the active infraction data migration. + + The active infraction data migration should do the following things: + + 1. migrates all active notes, warnings, and kicks to an inactive status; + 2. migrates all users with multiple active infractions of a single type to have only one active + infraction of that type. The infraction with the longest duration stays active. + """ + + app = "api" + migration_prior = "0043_infraction_hidden_warnings_to_notes" + migration_target = "0044_active_infractions_migration" + + @classmethod + def setUpMigrationData(cls, apps): + """Sets up an initial database state that contains the relevant test cases.""" + # Fetch the Infraction and User model in the current migration state + cls.infraction_model = apps.get_model('api', 'Infraction') + cls.user_model = apps.get_model('api', 'User') + + cls.created_infractions = {} + + # Moderator that serves as actor for all infractions + cls.user_moderator = cls.user_model.objects.create( + id=9999, + name="Olivier de Vienne", + discriminator=1040, + avatar_hash=None, + ) + + # User #1: clean user with no infractions + cls.created_infractions.update( + { + 1: InfractionFactory.create( + actor=cls.user_moderator, + infractions=[], + infraction_model=cls.infraction_model, + user_model=cls.user_model, + ) + } + ) + + # User #2: One inactive note infraction + cls.created_infractions.update( + { + 2: InfractionFactory.create( + actor=cls.user_moderator, + infractions=( + {'type': 'note', 'active': False, 'hidden': True}, + ), + infraction_model=cls.infraction_model, + user_model=cls.user_model, + ) + } + ) + + # User #3: One active note infraction + cls.created_infractions.update( + { + 3: InfractionFactory.create( + actor=cls.user_moderator, + infractions=( + {'type': 'note', 'active': True, 'hidden': True}, + ), + infraction_model=cls.infraction_model, + user_model=cls.user_model, + ) + } + ) + + # User #4: One active and one inactive note infraction + cls.created_infractions.update( + { + 4: InfractionFactory.create( + actor=cls.user_moderator, + infractions=( + {'type': 'note', 'active': False, 'hidden': True}, + {'type': 'note', 'active': True, 'hidden': True}, + ), + infraction_model=cls.infraction_model, + user_model=cls.user_model, + ) + } + ) + + # User #5: Once active note, one active kick, once active warning + cls.created_infractions.update( + { + 5: InfractionFactory.create( + actor=cls.user_moderator, + infractions=( + {'type': 'note', 'active': True, 'hidden': True}, + {'type': 'kick', 'active': True, 'hidden': True}, + {'type': 'warning', 'active': True, 'hidden': True}, + ), + infraction_model=cls.infraction_model, + user_model=cls.user_model, + ) + } + ) + + # User #6: One inactive ban and one active ban + cls.created_infractions.update( + { + 6: InfractionFactory.create( + actor=cls.user_moderator, + infractions=( + {'type': 'ban', 'active': False, 'hidden': True}, + {'type': 'ban', 'active': True, 'hidden': True}, + ), + infraction_model=cls.infraction_model, + user_model=cls.user_model, + ) + } + ) + + # User #7: Two active permanent bans + cls.created_infractions.update( + { + 7: InfractionFactory.create( + actor=cls.user_moderator, + infractions=( + {'type': 'ban', 'active': True, 'hidden': True}, + {'type': 'ban', 'active': True, 'hidden': True}, + ), + infraction_model=cls.infraction_model, + user_model=cls.user_model, + ) + } + ) + + # User #8: Multiple active temporary bans + cls.created_infractions.update( + { + 8: InfractionFactory.create( + actor=cls.user_moderator, + infractions=( + { + 'type': 'ban', + 'active': True, + 'hidden': True, + 'expires_at': timezone.now() + timedelta(days=1) + }, + { + 'type': 'ban', + 'active': True, + 'hidden': True, + 'expires_at': timezone.now() + timedelta(days=10) + }, + { + 'type': 'ban', + 'active': True, + 'hidden': True, + 'expires_at': timezone.now() + timedelta(days=20) + }, + { + 'type': 'ban', + 'active': True, + 'hidden': True, + 'expires_at': timezone.now() + timedelta(days=5) + }, + ), + infraction_model=cls.infraction_model, + user_model=cls.user_model, + ) + } + ) + + # User #9: One active permanent ban, two active temporary bans + cls.created_infractions.update( + { + 9: InfractionFactory.create( + actor=cls.user_moderator, + infractions=( + { + 'type': 'ban', + 'active': True, + 'hidden': True, + 'expires_at': timezone.now() + timedelta(days=10) + }, + { + 'type': 'ban', + 'active': True, + 'hidden': True, + 'expires_at': None, + }, + { + 'type': 'ban', + 'active': True, + 'hidden': True, + 'expires_at': timezone.now() + timedelta(days=7) + }, + ), + infraction_model=cls.infraction_model, + user_model=cls.user_model, + ) + } + ) + + # User #10: One inactive permanent ban, two active temporary bans + cls.created_infractions.update( + { + 10: InfractionFactory.create( + actor=cls.user_moderator, + infractions=( + { + 'type': 'ban', + 'active': True, + 'hidden': True, + 'expires_at': timezone.now() + timedelta(days=10) + }, + { + 'type': 'ban', + 'active': False, + 'hidden': True, + 'expires_at': None, + }, + { + 'type': 'ban', + 'active': True, + 'hidden': True, + 'expires_at': timezone.now() + timedelta(days=7) + }, + ), + infraction_model=cls.infraction_model, + user_model=cls.user_model, + ) + } + ) + + # User #11: Active ban, active mute, active superstar + cls.created_infractions.update( + { + 11: InfractionFactory.create( + actor=cls.user_moderator, + infractions=( + {'type': 'ban', 'active': True, 'hidden': True}, + {'type': 'mute', 'active': True, 'hidden': True}, + {'type': 'superstar', 'active': True, 'hidden': True}, + {'type': 'watch', 'active': True, 'hidden': True}, + ), + infraction_model=cls.infraction_model, + user_model=cls.user_model, + ) + } + ) + + # User #12: Multiple active bans, active mutes, active superstars + cls.created_infractions.update( + { + 12: InfractionFactory.create( + actor=cls.user_moderator, + infractions=( + {'type': 'ban', 'active': True, 'hidden': True}, + {'type': 'ban', 'active': True, 'hidden': True}, + {'type': 'ban', 'active': True, 'hidden': True}, + {'type': 'mute', 'active': True, 'hidden': True}, + {'type': 'mute', 'active': True, 'hidden': True}, + {'type': 'mute', 'active': True, 'hidden': True}, + {'type': 'superstar', 'active': True, 'hidden': True}, + {'type': 'superstar', 'active': True, 'hidden': True}, + {'type': 'superstar', 'active': True, 'hidden': True}, + {'type': 'watch', 'active': True, 'hidden': True}, + {'type': 'watch', 'active': True, 'hidden': True}, + {'type': 'watch', 'active': True, 'hidden': True}, + ), + infraction_model=cls.infraction_model, + user_model=cls.user_model, + ) + } + ) + + def test_all_never_active_types_became_inactive(self): + """Are all infractions of a non-active type inactive after the migration?""" + inactive_type_query = Q(type="note") | Q(type="warning") | Q(type="kick") + self.assertFalse( + self.infraction_model.objects.filter(inactive_type_query, active=True).exists() + ) + + def test_migration_left_clean_user_without_infractions(self): + """Do users without infractions have no infractions after the migration?""" + user_id, infraction_history = self.created_infractions[1] + self.assertFalse( + self.infraction_model.objects.filter(user__id=user_id).exists() + ) + + def test_migration_left_user_with_inactive_note_untouched(self): + """Did the migration leave users with only an inactive note untouched?""" + user_id, infraction_history = self.created_infractions[2] + inactive_note = infraction_history[0] + self.assertTrue( + self.infraction_model.objects.filter(**model_to_dict(inactive_note)).exists() + ) + + def test_migration_only_touched_active_field_of_active_note(self): + """Does the migration only change the `active` field?""" + user_id, infraction_history = self.created_infractions[3] + note = model_to_dict(infraction_history[0]) + note['active'] = False + self.assertTrue( + self.infraction_model.objects.filter(**note).exists() + ) + + def test_migration_only_touched_active_field_of_active_note_left_inactive_untouched(self): + """Does the migration only change the `active` field of active notes?""" + user_id, infraction_history = self.created_infractions[4] + for note in infraction_history: + with self.subTest(active=note.active): + note = model_to_dict(note) + note['active'] = False + self.assertTrue( + self.infraction_model.objects.filter(**note).exists() + ) + + def test_migration_migrates_all_nonactive_types_to_inactive(self): + """Do we set the `active` field of all non-active infractions to `False`?""" + user_id, infraction_history = self.created_infractions[5] + self.assertFalse( + self.infraction_model.objects.filter(user__id=user_id, active=True).exists() + ) + + def test_migration_leaves_user_with_one_active_ban_untouched(self): + """Do we leave a user with one active and one inactive ban untouched?""" + user_id, infraction_history = self.created_infractions[6] + for infraction in infraction_history: + with self.subTest(active=infraction.active): + self.assertTrue( + self.infraction_model.objects.filter(**model_to_dict(infraction)).exists() + ) + + def test_migration_turns_double_active_perm_ban_into_single_active_perm_ban(self): + """Does the migration turn two active permanent bans into one active permanent ban?""" + user_id, infraction_history = self.created_infractions[7] + active_count = self.infraction_model.objects.filter(user__id=user_id, active=True).count() + self.assertEqual(active_count, 1) + + def test_migration_leaves_temporary_ban_with_longest_duration_active(self): + """Does the migration turn two active permanent bans into one active permanent ban?""" + user_id, infraction_history = self.created_infractions[8] + active_ban = self.infraction_model.objects.get(user__id=user_id, active=True) + self.assertEqual(active_ban.expires_at, infraction_history[2].expires_at) + + def test_migration_leaves_permanent_ban_active(self): + """Does the migration leave the permanent ban active?""" + user_id, infraction_history = self.created_infractions[9] + active_ban = self.infraction_model.objects.get(user__id=user_id, active=True) + self.assertIsNone(active_ban.expires_at) + + def test_migration_leaves_longest_temp_ban_active_with_inactive_permanent_ban(self): + """Does the longest temp ban stay active, even with an inactive perm ban present?""" + user_id, infraction_history = self.created_infractions[10] + active_ban = self.infraction_model.objects.get(user__id=user_id, active=True) + self.assertEqual(active_ban.expires_at, infraction_history[0].expires_at) + + def test_migration_leaves_all_active_types_active_if_one_of_each_exists(self): + """Do all active infractions stay active if only one of each is present?""" + user_id, infraction_history = self.created_infractions[11] + active_count = self.infraction_model.objects.filter(user__id=user_id, active=True).count() + self.assertEqual(active_count, 4) + + def test_migration_reduces_all_active_types_to_a_single_active_infraction(self): + """Do we reduce all of the infraction types to one active infraction?""" + user_id, infraction_history = self.created_infractions[12] + active_infractions = self.infraction_model.objects.filter(user__id=user_id, active=True) + self.assertEqual(len(active_infractions), 4) + types_observed = [infraction.type for infraction in active_infractions] + + for infraction_type in ('ban', 'mute', 'superstar', 'watch'): + with self.subTest(type=infraction_type): + self.assertIn(infraction_type, types_observed) -- cgit v1.2.3 From 0d383cbe925fdec97cb678c0168ecf7e90d3d729 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Tue, 8 Oct 2019 01:22:15 +0200 Subject: Prevent double active infractions with constraint https://github.com/python-discord/site/issues/273 This commits adds a UniqueConstraint for active infractions on a combination of the `user` and `type` field. This means that a user can only have one active infraction of a given type in the database at any time. I've also added tests to make sure that this behaves as expected. --- ...45_add_infractions_unique_constraints_active.py | 17 +++++ pydis_site/apps/api/models/bot/infraction.py | 7 ++ pydis_site/apps/api/tests/test_infractions.py | 89 +++++++++++++++++++++- 3 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0045_add_infractions_unique_constraints_active.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0045_add_infractions_unique_constraints_active.py b/pydis_site/apps/api/migrations/0045_add_infractions_unique_constraints_active.py new file mode 100644 index 00000000..bacb56b8 --- /dev/null +++ b/pydis_site/apps/api/migrations/0045_add_infractions_unique_constraints_active.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.6 on 2019-10-07 18:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0044_active_infractions_migration'), + ] + + operations = [ + migrations.AddConstraint( + model_name='infraction', + constraint=models.UniqueConstraint(condition=models.Q(active=True), fields=('user', 'type'), name='unique_active_infraction_per_type_per_user'), + ), + ] diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py index dfb32a97..108fd3a2 100644 --- a/pydis_site/apps/api/models/bot/infraction.py +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -71,3 +71,10 @@ class Infraction(ModelReprMixin, models.Model): """Defines the meta options for the infraction model.""" ordering = ['-inserted_at'] + constraints = ( + models.UniqueConstraint( + fields=["user", "type"], + condition=models.Q(active=True), + name="unique_active_infraction_per_type_per_user" + ), + ) diff --git a/pydis_site/apps/api/tests/test_infractions.py b/pydis_site/apps/api/tests/test_infractions.py index 8279b6a6..69d9ebea 100644 --- a/pydis_site/apps/api/tests/test_infractions.py +++ b/pydis_site/apps/api/tests/test_infractions.py @@ -1,6 +1,7 @@ from datetime import datetime as dt, timedelta, timezone from urllib.parse import quote +from django.db.utils import IntegrityError from django_hosts.resolvers import reverse from .base import APISubdomainTestCase @@ -167,6 +168,12 @@ class CreationTests(APISubdomainTestCase): discriminator=1, avatar_hash=None ) + cls.second_user = User.objects.create( + id=6, + name='carl', + discriminator=2, + avatar_hash=None + ) def test_accepts_valid_data(self): url = reverse('bot:infraction-list', host='api') @@ -390,6 +397,82 @@ class CreationTests(APISubdomainTestCase): second_response = self.client.post(url, data=second_active_infraction) self.assertEqual(second_response.status_code, 201) + def test_unique_constraint_raises_integrity_error_on_second_active_of_same_type(self): + """Do we raise `IntegrityError` for the second active infraction of a type for a user?""" + Infraction.objects.create( + user=self.user, + actor=self.user, + type="ban", + active=True, + reason="The first active ban" + ) + with self.assertRaises(IntegrityError): + Infraction.objects.create( + user=self.user, + actor=self.user, + type="ban", + active=True, + reason="The second active ban" + ) + + def test_unique_constraint_accepts_active_infraction_after_inactive_infraction(self): + """Do we accept an active infraction if the others of the same type are inactive?""" + Infraction.objects.create( + user=self.user, + actor=self.user, + type="ban", + active=False, + reason="The first inactive ban" + ) + Infraction.objects.create( + user=self.user, + actor=self.user, + type="ban", + active=False, + reason="The second inactive ban" + ) + Infraction.objects.create( + user=self.user, + actor=self.user, + type="ban", + active=True, + reason="The first active ban" + ) + + def test_unique_constraint_accepts_second_active_of_different_type(self): + """Do we accept a second active infraction of a different type for a given user?""" + Infraction.objects.create( + user=self.user, + actor=self.user, + type="ban", + active=True, + reason="The first active ban" + ) + Infraction.objects.create( + user=self.user, + actor=self.user, + type="mute", + active=True, + reason="The first active mute" + ) + + def test_unique_constraint_accepts_active_infractions_for_different_users(self): + """Do we accept two active infractions of the same type for two different users?""" + Infraction.objects.create( + user=self.user, + actor=self.user, + type="ban", + active=True, + reason="An active ban for the first user" + ) + Infraction.objects.create( + user=self.second_user, + actor=self.second_user, + type="ban", + active=False, + reason="An active ban for the second user" + ) + class ExpandedTests(APISubdomainTestCase): @classmethod @@ -403,12 +486,14 @@ class ExpandedTests(APISubdomainTestCase): cls.kick = Infraction.objects.create( user_id=cls.user.id, actor_id=cls.user.id, - type='kick' + type='kick', + active=False ) cls.warning = Infraction.objects.create( user_id=cls.user.id, actor_id=cls.user.id, - type='warning' + type='warning', + active=False, ) def check_expanded_fields(self, infraction): -- cgit v1.2.3 From e415428386caae19aa0398aea66ee5053569abcf Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Fri, 18 Oct 2019 12:52:11 +0200 Subject: Solve migration conflict by renaming migrations The migration files were generated and named before the migrations added by other pull requests. This caused the migration path to diverge. Since the migrations did not touch the same models, the solution was to rename the migration files to place them at the end of the migration history. --- .../0044_active_infractions_migration.py | 106 --------------------- ...45_add_infractions_unique_constraints_active.py | 17 ---- .../0046_active_infractions_migration.py | 106 +++++++++++++++++++++ ...47_add_infractions_unique_constraints_active.py | 17 ++++ .../migrations/test_active_infraction_migration.py | 8 +- 5 files changed, 127 insertions(+), 127 deletions(-) delete mode 100644 pydis_site/apps/api/migrations/0044_active_infractions_migration.py delete mode 100644 pydis_site/apps/api/migrations/0045_add_infractions_unique_constraints_active.py create mode 100644 pydis_site/apps/api/migrations/0046_active_infractions_migration.py create mode 100644 pydis_site/apps/api/migrations/0047_add_infractions_unique_constraints_active.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0044_active_infractions_migration.py b/pydis_site/apps/api/migrations/0044_active_infractions_migration.py deleted file mode 100644 index 14746712..00000000 --- a/pydis_site/apps/api/migrations/0044_active_infractions_migration.py +++ /dev/null @@ -1,106 +0,0 @@ -# Generated by Django 2.2.6 on 2019-10-07 15:59 - -from django.db import migrations -from django.db.models import Count, Prefetch, Q, QuerySet - - -class ExpirationWrapper: - """Wraps an expiration date to properly compare permanent and temporary infractions.""" - - def __init__(self, infraction): - self.expiration_date = infraction.expires_at - - def __lt__(self, other): - """An `expiration_date` is considered smaller when it comes earlier than the `other`.""" - if self.expiration_date is None: - # A permanent infraction can never end sooner than another infraction - return False - elif other.expiration_date is None: - # If `self` is temporary, but `other` is permanent, `self` is smaller - return True - else: - return self.expiration_date < other.expiration_date - - def __eq__(self, other): - """If both expiration dates are permanent they're equal, otherwise compare dates.""" - if self.expiration_date is None and other.expiration_date is None: - return True - elif self.expiration_date is None or other.expiration_date is None: - return False - else: - return self.expiration_date == other.expiration_date - - -def migrate_inactive_types_to_inactive(apps, schema_editor): - """Migrates infractions of non-active types to inactive.""" - inactive_infraction_types = Q(type="note") | Q(type="warning") | Q(type="kick") - infraction_model = apps.get_model('api', 'Infraction') - infraction_model.objects.filter(inactive_infraction_types).update(active=False) - - -def get_query(user_model, infraction_model, infr_type: str) -> QuerySet: - """ - Creates QuerySet to fetch users with multiple active infractions of the given `type`. - - The QuerySet will prefetch the infractions and attach them as an `.infractions` attribute to the - `User` instances. - """ - active_infractions = infraction_model.objects.filter(type=infr_type, active=True) - - # Build an SQL query by chaining methods together - - # Get users with active infraction(s) of the provided `infr_type` - query = user_model.objects.filter( - Q(infractions_received__type=infr_type, infractions_received__active=True) - ) - - # Prefetch their active received infractions of `infr_type` and attach `.infractions` attribute - query = query.prefetch_related( - Prefetch('infractions_received', queryset=active_infractions, to_attr='infractions') - ) - - # Count and only include them if they have at least 2 active infractions of the `type` - query = query.annotate(num_infractions=Count('infractions_received')) - query = query.filter(num_infractions__gte=2) - - # Make sure we return each individual only once - query = query.distinct() - - return query - - -def migrate_multiple_active_infractions_per_user_to_one(apps, schema_editor): - """ - Make sure a user only has one active infraction of a given "active" infraction type. - - If a user has multiple active infraction, we keep the one with longest expiration date active - and migrate the others to inactive. - """ - infraction_model = apps.get_model('api', 'Infraction') - user_model = apps.get_model('api', 'User') - - for infraction_type in ('ban', 'mute', 'superstar', 'watch'): - query = get_query(user_model, infraction_model, infraction_type) - for user in query: - infractions = sorted(user.infractions, key=ExpirationWrapper, reverse=True) - for infraction in infractions[1:]: - infraction.active = False - infraction.save() - - -def reverse_migration(apps, schema_editor): - """There's no need to do anything special to reverse these migrations.""" - return - - -class Migration(migrations.Migration): - """Data migration to get the database consistent with the new infraction validation rules.""" - - dependencies = [ - ('api', '0043_infraction_hidden_warnings_to_notes'), - ] - - operations = [ - migrations.RunPython(migrate_inactive_types_to_inactive, reverse_migration), - migrations.RunPython(migrate_multiple_active_infractions_per_user_to_one, reverse_migration) - ] diff --git a/pydis_site/apps/api/migrations/0045_add_infractions_unique_constraints_active.py b/pydis_site/apps/api/migrations/0045_add_infractions_unique_constraints_active.py deleted file mode 100644 index bacb56b8..00000000 --- a/pydis_site/apps/api/migrations/0045_add_infractions_unique_constraints_active.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.6 on 2019-10-07 18:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0044_active_infractions_migration'), - ] - - operations = [ - migrations.AddConstraint( - model_name='infraction', - constraint=models.UniqueConstraint(condition=models.Q(active=True), fields=('user', 'type'), name='unique_active_infraction_per_type_per_user'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0046_active_infractions_migration.py b/pydis_site/apps/api/migrations/0046_active_infractions_migration.py new file mode 100644 index 00000000..7e5a631c --- /dev/null +++ b/pydis_site/apps/api/migrations/0046_active_infractions_migration.py @@ -0,0 +1,106 @@ +# Generated by Django 2.2.6 on 2019-10-07 15:59 + +from django.db import migrations +from django.db.models import Count, Prefetch, Q, QuerySet + + +class ExpirationWrapper: + """Wraps an expiration date to properly compare permanent and temporary infractions.""" + + def __init__(self, infraction): + self.expiration_date = infraction.expires_at + + def __lt__(self, other): + """An `expiration_date` is considered smaller when it comes earlier than the `other`.""" + if self.expiration_date is None: + # A permanent infraction can never end sooner than another infraction + return False + elif other.expiration_date is None: + # If `self` is temporary, but `other` is permanent, `self` is smaller + return True + else: + return self.expiration_date < other.expiration_date + + def __eq__(self, other): + """If both expiration dates are permanent they're equal, otherwise compare dates.""" + if self.expiration_date is None and other.expiration_date is None: + return True + elif self.expiration_date is None or other.expiration_date is None: + return False + else: + return self.expiration_date == other.expiration_date + + +def migrate_inactive_types_to_inactive(apps, schema_editor): + """Migrates infractions of non-active types to inactive.""" + inactive_infraction_types = Q(type="note") | Q(type="warning") | Q(type="kick") + infraction_model = apps.get_model('api', 'Infraction') + infraction_model.objects.filter(inactive_infraction_types).update(active=False) + + +def get_query(user_model, infraction_model, infr_type: str) -> QuerySet: + """ + Creates QuerySet to fetch users with multiple active infractions of the given `type`. + + The QuerySet will prefetch the infractions and attach them as an `.infractions` attribute to the + `User` instances. + """ + active_infractions = infraction_model.objects.filter(type=infr_type, active=True) + + # Build an SQL query by chaining methods together + + # Get users with active infraction(s) of the provided `infr_type` + query = user_model.objects.filter( + Q(infractions_received__type=infr_type, infractions_received__active=True) + ) + + # Prefetch their active received infractions of `infr_type` and attach `.infractions` attribute + query = query.prefetch_related( + Prefetch('infractions_received', queryset=active_infractions, to_attr='infractions') + ) + + # Count and only include them if they have at least 2 active infractions of the `type` + query = query.annotate(num_infractions=Count('infractions_received')) + query = query.filter(num_infractions__gte=2) + + # Make sure we return each individual only once + query = query.distinct() + + return query + + +def migrate_multiple_active_infractions_per_user_to_one(apps, schema_editor): + """ + Make sure a user only has one active infraction of a given "active" infraction type. + + If a user has multiple active infraction, we keep the one with longest expiration date active + and migrate the others to inactive. + """ + infraction_model = apps.get_model('api', 'Infraction') + user_model = apps.get_model('api', 'User') + + for infraction_type in ('ban', 'mute', 'superstar', 'watch'): + query = get_query(user_model, infraction_model, infraction_type) + for user in query: + infractions = sorted(user.infractions, key=ExpirationWrapper, reverse=True) + for infraction in infractions[1:]: + infraction.active = False + infraction.save() + + +def reverse_migration(apps, schema_editor): + """There's no need to do anything special to reverse these migrations.""" + return + + +class Migration(migrations.Migration): + """Data migration to get the database consistent with the new infraction validation rules.""" + + dependencies = [ + ('api', '0045_add_plural_name_for_log_entry'), + ] + + operations = [ + migrations.RunPython(migrate_inactive_types_to_inactive, reverse_migration), + migrations.RunPython(migrate_multiple_active_infractions_per_user_to_one, reverse_migration) + ] diff --git a/pydis_site/apps/api/migrations/0047_add_infractions_unique_constraints_active.py b/pydis_site/apps/api/migrations/0047_add_infractions_unique_constraints_active.py new file mode 100644 index 00000000..dc266474 --- /dev/null +++ b/pydis_site/apps/api/migrations/0047_add_infractions_unique_constraints_active.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.6 on 2019-10-07 18:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0046_active_infractions_migration'), + ] + + operations = [ + migrations.AddConstraint( + model_name='infraction', + constraint=models.UniqueConstraint(condition=models.Q(active=True), fields=('user', 'type'), name='unique_active_infraction_per_type_per_user'), + ), + ] diff --git a/pydis_site/apps/api/tests/migrations/test_active_infraction_migration.py b/pydis_site/apps/api/tests/migrations/test_active_infraction_migration.py index cfae4219..61574b7e 100644 --- a/pydis_site/apps/api/tests/migrations/test_active_infraction_migration.py +++ b/pydis_site/apps/api/tests/migrations/test_active_infraction_migration.py @@ -81,8 +81,8 @@ class InfractionFactoryTests(MigrationsTestCase): """Tests for the InfractionFactory.""" app = "api" - migration_prior = "0043_infraction_hidden_warnings_to_notes" - migration_target = "0043_infraction_hidden_warnings_to_notes" + migration_prior = "0045_add_plural_name_for_log_entry" + migration_target = "0045_add_plural_name_for_log_entry" @classmethod def setUpPostMigrationData(cls, apps): @@ -183,8 +183,8 @@ class ActiveInfractionMigrationTests(MigrationsTestCase): """ app = "api" - migration_prior = "0043_infraction_hidden_warnings_to_notes" - migration_target = "0044_active_infractions_migration" + migration_prior = "0045_add_plural_name_for_log_entry" + migration_target = "0046_active_infractions_migration" @classmethod def setUpMigrationData(cls, apps): -- cgit v1.2.3 From 71b40842ddf9c72d34921cae0c294bcc9f0c4672 Mon Sep 17 00:00:00 2001 From: Akarys42 Date: Sat, 19 Oct 2019 12:38:18 +0200 Subject: Add a jump-url field in the reminder model --- pydis_site/apps/api/models/bot/reminder.py | 5 +++++ pydis_site/apps/api/serializers.py | 2 +- pydis_site/apps/api/tests/test_models.py | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/bot/reminder.py b/pydis_site/apps/api/models/bot/reminder.py index decc9391..026d3a3a 100644 --- a/pydis_site/apps/api/models/bot/reminder.py +++ b/pydis_site/apps/api/models/bot/reminder.py @@ -15,6 +15,11 @@ class Reminder(ModelReprMixin, models.Model): "If not, it has been sent out to the user." ) ) + jump_url = models.CharField( + help_text=( + "The jump url to the message that created the reminder" + ) + ) author = models.ForeignKey( User, on_delete=models.CASCADE, diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 326e20e1..8a605612 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -190,7 +190,7 @@ class ReminderSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = Reminder - fields = ('active', 'author', 'channel_id', 'content', 'expiration', 'id') + fields = ('active', 'author', 'jump_url', 'channel_id', 'content', 'expiration', 'id') class RoleSerializer(ModelSerializer): diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index bce76942..b4a766d0 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -115,6 +115,10 @@ class StringDunderMethodTests(SimpleTestCase): discriminator=5, avatar_hash=None ), channel_id=555, + jump_url=( + 'https://discordapp.com/channels/' + '267624335836053506/291284109232308226/463087129459949587' + ), content="oh no", expiration=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc) ) -- cgit v1.2.3 From 09f923b3324b685ea3e9c3cfa715eeb877dd9bd0 Mon Sep 17 00:00:00 2001 From: Gareth Coles Date: Sat, 19 Oct 2019 12:13:37 +0100 Subject: Initial navbar change to add settings button next to logout --- pydis_site/static/css/base/base.css | 3 +-- pydis_site/templates/base/navbar.html | 9 ++++++++- 2 files changed, 9 insertions(+), 3 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/static/css/base/base.css b/pydis_site/static/css/base/base.css index 3ca6b2a7..24282628 100644 --- a/pydis_site/static/css/base/base.css +++ b/pydis_site/static/css/base/base.css @@ -84,7 +84,6 @@ div.card.has-equal-height { /* Fix for logout form submit button in navbar */ -button.is-size-navbar-menu { +button.is-size-navbar-menu, a.is-size-navbar-menu { font-size: 14px; } - diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html index 8cdac0de..f0041b6b 100644 --- a/pydis_site/templates/base/navbar.html +++ b/pydis_site/templates/base/navbar.html @@ -102,7 +102,14 @@ {% else %}
{% csrf_token %} - +
{% endif %} -- cgit v1.2.3 From 2d20f4ab8d3202e668e3e78c4bb6e8b6703ef5f9 Mon Sep 17 00:00:00 2001 From: Akarys42 Date: Sat, 19 Oct 2019 13:22:27 +0200 Subject: Add max_lengh for reminder.jump_url --- pydis_site/apps/api/models/bot/reminder.py | 1 + 1 file changed, 1 insertion(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/bot/reminder.py b/pydis_site/apps/api/models/bot/reminder.py index 026d3a3a..3b174252 100644 --- a/pydis_site/apps/api/models/bot/reminder.py +++ b/pydis_site/apps/api/models/bot/reminder.py @@ -16,6 +16,7 @@ class Reminder(ModelReprMixin, models.Model): ) ) jump_url = models.CharField( + max_length=88, help_text=( "The jump url to the message that created the reminder" ) -- cgit v1.2.3 From 7bdd61049e952671e1bd3c809545bb8cecb3956f Mon Sep 17 00:00:00 2001 From: Gareth Coles Date: Sat, 19 Oct 2019 15:15:40 +0100 Subject: Bring navbar styling in line on mobile as well --- pydis_site/static/css/base/base.css | 9 +++++++++ pydis_site/templates/base/navbar.html | 1 + 2 files changed, 10 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/static/css/base/base.css b/pydis_site/static/css/base/base.css index 24282628..7db9499d 100644 --- a/pydis_site/static/css/base/base.css +++ b/pydis_site/static/css/base/base.css @@ -86,4 +86,13 @@ div.card.has-equal-height { button.is-size-navbar-menu, a.is-size-navbar-menu { font-size: 14px; + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +@media screen and (min-width: 1088px) { + button.is-size-navbar-menu, a.is-size-navbar-menu { + padding-left: 1rem; + padding-right: 1rem; + } } diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html index f0041b6b..f1a3f928 100644 --- a/pydis_site/templates/base/navbar.html +++ b/pydis_site/templates/base/navbar.html @@ -102,6 +102,7 @@ {% else %}
{% csrf_token %} +
- diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index 1ee93b10..d153b293 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -105,4 +105,3 @@ {% endblock %} - diff --git a/pydis_site/templates/wiki/history.html b/pydis_site/templates/wiki/history.html index 3788385f..ee297bdd 100644 --- a/pydis_site/templates/wiki/history.html +++ b/pydis_site/templates/wiki/history.html @@ -124,5 +124,3 @@ {% endblock %} - - -- cgit v1.2.3 From e77259351da7f18a375aa9ce4801755efca6c65b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 20 Mar 2020 19:07:57 -0700 Subject: Use basename instead of base_name with DRF router base_name was deprecated in 3.9 and finally removed in 3.11. --- pydis_site/apps/api/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index 4a0281b4..3bb5198e 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -41,7 +41,7 @@ bot_router.register( bot_router.register( 'off-topic-channel-names', OffTopicChannelNameViewSet, - base_name='offtopicchannelname' + basename='offtopicchannelname' ) bot_router.register( 'reminders', -- cgit v1.2.3 From 145beb37fcb4fa2f487f18b234dd72bc4e10c279 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 21 Mar 2020 10:04:44 -0700 Subject: Allow empty list for message embeds By default, blank=False for ArrayFields but allow_empty=True for ListField. Before DRF 3.10 there was a bug that ListField didn't respect the default value of blank=False and thus created a ListField in the serialiser with the default of allow_empty=True. We were relying on the behaviour of that bug. See encode/django-rest-framework#6597. --- .../migrations/0051_allow_blank_message_embeds.py | 21 +++++++++++++++++++++ pydis_site/apps/api/models/bot/message.py | 1 + 2 files changed, 22 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py new file mode 100644 index 00000000..e617e1c9 --- /dev/null +++ b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.4 on 2020-03-21 17:05 + +import django.contrib.postgres.fields +import django.contrib.postgres.fields.jsonb +from django.db import migrations +import pydis_site.apps.api.models.bot.tag + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0050_remove_infractions_active_default_value'), + ] + + operations = [ + migrations.AlterField( + model_name='deletedmessage', + name='embeds', + field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]), blank=True, help_text='Embeds attached to this message.', size=None), + ), + ] diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index 8b18fc9f..0b279580 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -49,6 +49,7 @@ class Message(ModelReprMixin, models.Model): pgfields.JSONField( validators=(validate_tag_embed,) ), + blank=True, help_text="Embeds attached to this message." ) attachments = pgfields.ArrayField( -- cgit v1.2.3 From df80fe2b39a8bfbeaa8db4c03bff0a49005913da Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Fri, 27 Mar 2020 15:16:10 +0100 Subject: Add events section to menu dropdown To draw attention to upcoming events, I've changed the events section of the "more" dropdown menu. It now has an events header, followed by the upcoming event, and a link to the general events page. --- pydis_site/templates/base/navbar.html | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html index 376dab5a..9b475189 100644 --- a/pydis_site/templates/base/navbar.html +++ b/pydis_site/templates/base/navbar.html @@ -84,8 +84,14 @@ Privacy - - Code Jams + + + Upcoming: Game Jam 2020 + + + All events -- cgit v1.2.3 From b3ab6441aa60d057fe0b8dcf44832f76a90b5b5b Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Fri, 27 Mar 2020 17:41:00 +0100 Subject: Add Game Jam banner to landing page I've added the Game Jam banner to the home page and made it link the Game Jam info page. --- pydis_site/templates/home/index.html | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index d153b293..bd713e55 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -38,14 +38,10 @@ {# Right column container #} -
- +
-- cgit v1.2.3 From 46ef497fd028531ffac8ebc19dec41a3272f72a3 Mon Sep 17 00:00:00 2001 From: ks123 Date: Mon, 30 Mar 2020 13:30:00 +0300 Subject: (Off-topic Channel Names): Added new field to model: `used` that show is this name already used on this round of names, added migration for this. --- .../api/migrations/0051_offtopicchannelname_used.py | 18 ++++++++++++++++++ .../apps/api/models/bot/off_topic_channel_name.py | 5 +++++ 2 files changed, 23 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py new file mode 100644 index 00000000..74836d8c --- /dev/null +++ b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.11 on 2020-03-30 10:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0050_remove_infractions_active_default_value'), + ] + + operations = [ + migrations.AddField( + model_name='offtopicchannelname', + name='used', + field=models.BooleanField(default=False, help_text='Show is channel already used as channel name in this round.'), + ), + ] diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 29280c27..3345754d 100644 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -16,6 +16,11 @@ class OffTopicChannelName(ModelReprMixin, models.Model): help_text="The actual channel name that will be used on our Discord server." ) + used = models.BooleanField( + default=False, + help_text="Show is channel already used as channel name in this round." + ) + def __str__(self): """Returns the current off-topic name, for display purposes.""" return self.name -- cgit v1.2.3 From a3d2938d63443ee6fa43751f42c935b22d4efb47 Mon Sep 17 00:00:00 2001 From: ks123 Date: Mon, 30 Mar 2020 17:28:44 +0300 Subject: (Off-topic Channel Names Viewset): Added documentation about new `mark_used` query parameter, added implementation of this param. --- .../api/viewsets/bot/off_topic_channel_name.py | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index d6da2399..4328c894 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -21,6 +21,10 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): If the `random_items` query parameter is given, for example using... $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?random_items=5 ... then the API will return `5` random items from the database. + If the `mark_used` query parameter is given like... + $ curl api.pydis.local:8000/bot/off-topic-channel-names?random_items=5&mark_used=true + ... then the API will mark returned `5` items `used`. + When running out of names, API will mark all names to not used and start new round. #### Response format Return a list of off-topic-channel names: @@ -106,6 +110,31 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) + if 'mark_used' in request.query_params and request.query_params['mark_used']: + queryset = self.get_queryset().order_by('?').exclude(used=True)[:random_count] + self.get_queryset().filter( + name__in=(query.name for query in queryset) + ).update(used=True) + + # When client request more channel names than non-used names is available, start + # new round of names. + if len(queryset) < random_count: + # Get how much names still missing and don't fetch duplicate names. + need_more = random_count - len(queryset) + ext = self.get_queryset().order_by('?').exclude( + name__in=(query.name for query in queryset) + )[:need_more] + + # Set all names `used` field to False except these that we just used. + self.get_queryset().exclude(name__in=( + query.name for query in ext) + ).update(used=False) + # Join original queryset (that had missing names) + # and extension with these missing names. + queryset = list(queryset) + list(ext) + serialized = self.serializer_class(queryset, many=True) + return Response(serialized.data) + queryset = self.get_queryset().order_by('?')[:random_count] serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From 81516be86a22ba1ea6df419f0e4b4277adecae09 Mon Sep 17 00:00:00 2001 From: ks123 Date: Tue, 31 Mar 2020 13:44:38 +0300 Subject: (Off-topic Channel Names Viewset Tests): Added test for not authenticated request. --- pydis_site/apps/api/tests/test_off_topic_channel_names.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index bd42cd81..9697f1f6 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -21,6 +21,12 @@ class UnauthenticatedTests(APISubdomainTestCase): self.assertEqual(response.status_code, 401) + def test_cannot_read_off_topic_channel_name_list_with_random_item_and_mark_used_param(self): + url = reverse('bot:offtopicchannelname-list', host='api') + response = self.client.get(f'{url}?random_items=no&mark_used=true') + + self.assertEqual(response.status_code, 401) + class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_empty_object(self): -- cgit v1.2.3 From 1f66694835013b1025a1630afe3290adbc03b94f Mon Sep 17 00:00:00 2001 From: ks123 Date: Tue, 31 Mar 2020 13:45:46 +0300 Subject: (Off-topic Channel Names Viewset Tests): Added used parameter to list tests. --- pydis_site/apps/api/tests/test_off_topic_channel_names.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index 9697f1f6..fea3932b 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -65,8 +65,8 @@ class EmptyDatabaseTests(APISubdomainTestCase): class ListTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): - cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand') - cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') + cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand', used=False) + cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True) def test_returns_name_in_list(self): url = reverse('bot:offtopicchannelname-list', host='api') -- cgit v1.2.3 From 02315e7339858ae6a11bc742446a14b7220894ca Mon Sep 17 00:00:00 2001 From: ks123 Date: Tue, 31 Mar 2020 13:50:39 +0300 Subject: (Off-topic Channel Names Viewset Tests): Added test for default handling `mark_used` parameter. --- pydis_site/apps/api/tests/test_off_topic_channel_names.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index fea3932b..be0d5001 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -88,6 +88,13 @@ class ListTests(APISubdomainTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 1) + def test_returns_single_correct_item_with_mark_used_parameter_true_and_random_items_1(self): + url = reverse('bot:offtopicchannelname-list', host='api') + response = self.client.get(f'{url}?random_items=1&mark_used=true') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), [self.test_name.name]) + class CreationTests(APISubdomainTestCase): def setUp(self): -- cgit v1.2.3 From d6f657143fa38657fb0432b9adb779969e920c64 Mon Sep 17 00:00:00 2001 From: ks123 Date: Tue, 31 Mar 2020 13:54:59 +0300 Subject: (Off-topic Channel Names Viewset Tests): Added test for handling running out of names. --- pydis_site/apps/api/tests/test_off_topic_channel_names.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index be0d5001..06624d89 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -95,6 +95,13 @@ class ListTests(APISubdomainTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), [self.test_name.name]) + def test_running_out_of_names_with_mark_used_parameter(self): + url = reverse('bot:offtopicchannelname-list', host='api') + response = self.client.get(f'{url}?random_items=2&mark_used=true') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name]) + class CreationTests(APISubdomainTestCase): def setUp(self): -- cgit v1.2.3 From 42e861fc2a9d476ade79c7b72d4a883d3c498d90 Mon Sep 17 00:00:00 2001 From: kwzrd Date: Thu, 2 Apr 2020 18:08:13 +0200 Subject: Fix broken link to Game Jam banner The asset was recently moved to a new directory. --- pydis_site/templates/home/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index bd713e55..c30cbee6 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -40,7 +40,7 @@ {# Right column container #} -- cgit v1.2.3 From 0c1c59c0df9d7783e88482c8014a9f33f307cbd0 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 20 Apr 2020 09:18:47 +0300 Subject: Added `news` to allowed bot setting names. --- pydis_site/apps/api/models/bot/bot_setting.py | 1 + 1 file changed, 1 insertion(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/bot/bot_setting.py b/pydis_site/apps/api/models/bot/bot_setting.py index b1c3e47c..8d48eac7 100644 --- a/pydis_site/apps/api/models/bot/bot_setting.py +++ b/pydis_site/apps/api/models/bot/bot_setting.py @@ -9,6 +9,7 @@ def validate_bot_setting_name(name: str) -> None: """Raises a ValidationError if the given name is not a known setting.""" known_settings = ( 'defcon', + 'news', ) if name not in known_settings: -- cgit v1.2.3 From 36f9f1f1e116d37d28b6f049255d9c394da8097f Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 20 Apr 2020 09:55:40 +0300 Subject: Created migration fo Python News feature on bot --- .../api/migrations/0051_create_news_setting.py | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0051_create_news_setting.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py new file mode 100644 index 00000000..3ba169c9 --- /dev/null +++ b/pydis_site/apps/api/migrations/0051_create_news_setting.py @@ -0,0 +1,26 @@ +from django.db import migrations + + +def up(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + setting = BotSetting( + name='news', + data={} + ).save() + + +def down(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + BotSetting.get(name='news').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0050_remove_infractions_active_default_value'), + ] + + operations = [ + migrations.RunPython(up, down) + ] + -- cgit v1.2.3 From 86538bba645e66caaef653ecdcef217e7ff1d39a Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Tue, 21 Apr 2020 08:49:47 +0300 Subject: Fixed migration 51 file end (`create_news_setting`) --- pydis_site/apps/api/migrations/0051_create_news_setting.py | 1 - 1 file changed, 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py index 3ba169c9..f8522e02 100644 --- a/pydis_site/apps/api/migrations/0051_create_news_setting.py +++ b/pydis_site/apps/api/migrations/0051_create_news_setting.py @@ -23,4 +23,3 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython(up, down) ] - -- cgit v1.2.3 From 0dbc6e9165d188c55966eb267752eda42d274a0c Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Tue, 21 Apr 2020 15:51:06 +0300 Subject: Fixed migration nr 51 `down` function object getting --- pydis_site/apps/api/migrations/0051_create_news_setting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py index f8522e02..f18fdfb1 100644 --- a/pydis_site/apps/api/migrations/0051_create_news_setting.py +++ b/pydis_site/apps/api/migrations/0051_create_news_setting.py @@ -11,7 +11,7 @@ def up(apps, schema_editor): def down(apps, schema_editor): BotSetting = apps.get_model('api', 'BotSetting') - BotSetting.get(name='news').delete() + BotSetting.objects.get(name='news').delete() class Migration(migrations.Migration): -- cgit v1.2.3 From 2e6c22b996aa294e71adb699877b574df8a103ec Mon Sep 17 00:00:00 2001 From: Den4200 Date: Tue, 12 May 2020 11:58:15 -0400 Subject: Update Game Jam from upcoming to most recent --- pydis_site/templates/base/navbar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html index 9b475189..d2ea9589 100644 --- a/pydis_site/templates/base/navbar.html +++ b/pydis_site/templates/base/navbar.html @@ -88,7 +88,7 @@ Events - Upcoming: Game Jam 2020 + Most Recent: Game Jam 2020 All events -- cgit v1.2.3 From 354d78ea01b11f8197dd6933be460f9b277e4645 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Wed, 27 May 2020 09:24:48 +0200 Subject: No longer accept or track avatar_hash. This should completely remove avatar_hash from the site - both in our tests, in the model itself, and from the database (as a result of the migration). --- .../api/migrations/0052_remove_user_avatar_hash.py | 17 +++++++ pydis_site/apps/api/models/bot/user.py | 8 ---- pydis_site/apps/api/serializers.py | 2 +- pydis_site/apps/api/tests/test_deleted_messages.py | 2 - pydis_site/apps/api/tests/test_infractions.py | 7 +-- pydis_site/apps/api/tests/test_models.py | 53 ++++++++++++++-------- pydis_site/apps/api/tests/test_nominations.py | 2 - pydis_site/apps/api/tests/test_reminders.py | 4 -- pydis_site/apps/api/tests/test_users.py | 8 ---- pydis_site/apps/api/viewsets/bot/user.py | 5 -- pydis_site/apps/home/tests/test_signal_listener.py | 5 -- pydis_site/apps/staff/tests/test_logs_view.py | 1 - 12 files changed, 52 insertions(+), 62 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py new file mode 100644 index 00000000..26b3b954 --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.11 on 2020-05-27 07:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_create_news_setting'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='avatar_hash', + ), + ] diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index 5140d2bf..65e8751e 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -31,14 +31,6 @@ class User(ModelReprMixin, models.Model): ), help_text="The discriminator of this user, taken from Discord." ) - avatar_hash = models.CharField( - max_length=100, - help_text=( - "The user's avatar hash, taken from Discord. " - "Null if the user does not have any custom avatar." - ), - null=True - ) roles = models.ManyToManyField( Role, help_text="Any roles this user has on our server." diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index e11c4af2..cc3f167d 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -235,7 +235,7 @@ class UserSerializer(BulkSerializerMixin, ModelSerializer): """Metadata defined for the Django REST Framework.""" model = User - fields = ('id', 'avatar_hash', 'name', 'discriminator', 'roles', 'in_guild') + fields = ('id', 'name', 'discriminator', 'roles', 'in_guild') depth = 1 diff --git a/pydis_site/apps/api/tests/test_deleted_messages.py b/pydis_site/apps/api/tests/test_deleted_messages.py index fb93cae6..f079a8dd 100644 --- a/pydis_site/apps/api/tests/test_deleted_messages.py +++ b/pydis_site/apps/api/tests/test_deleted_messages.py @@ -13,7 +13,6 @@ class DeletedMessagesWithoutActorTests(APISubdomainTestCase): id=55, name='Robbie Rotten', discriminator=55, - avatar_hash=None ) cls.data = { @@ -54,7 +53,6 @@ class DeletedMessagesWithActorTests(APISubdomainTestCase): id=12904, name='Joe Armstrong', discriminator=1245, - avatar_hash=None ) cls.data = { diff --git a/pydis_site/apps/api/tests/test_infractions.py b/pydis_site/apps/api/tests/test_infractions.py index bc258b77..93ef8171 100644 --- a/pydis_site/apps/api/tests/test_infractions.py +++ b/pydis_site/apps/api/tests/test_infractions.py @@ -47,7 +47,6 @@ class InfractionTests(APISubdomainTestCase): id=5, name='james', discriminator=1, - avatar_hash=None ) cls.ban_hidden = Infraction.objects.create( user_id=cls.user.id, @@ -169,13 +168,11 @@ class CreationTests(APISubdomainTestCase): id=5, name='james', discriminator=1, - avatar_hash=None ) cls.second_user = User.objects.create( id=6, name='carl', discriminator=2, - avatar_hash=None ) def test_accepts_valid_data(self): @@ -522,7 +519,6 @@ class ExpandedTests(APISubdomainTestCase): id=5, name='james', discriminator=1, - avatar_hash=None ) cls.kick = Infraction.objects.create( user_id=cls.user.id, @@ -540,7 +536,7 @@ class ExpandedTests(APISubdomainTestCase): def check_expanded_fields(self, infraction): for key in ('user', 'actor'): obj = infraction[key] - for field in ('id', 'name', 'discriminator', 'avatar_hash', 'roles', 'in_guild'): + for field in ('id', 'name', 'discriminator', 'roles', 'in_guild'): self.assertTrue(field in obj, msg=f'field "{field}" missing from {key}') def test_list_expanded(self): @@ -599,7 +595,6 @@ class SerializerTests(APISubdomainTestCase): id=5, name='james', discriminator=1, - avatar_hash=None ) def create_infraction(self, _type: str, active: bool): diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index a97d3251..b4754484 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -39,12 +39,14 @@ class StringDunderMethodTests(SimpleTestCase): self.nomination = Nomination( id=123, actor=User( - id=9876, name='Mr. Hemlock', - discriminator=6666, avatar_hash=None + id=9876, + name='Mr. Hemlock', + discriminator=6666, ), user=User( - id=9876, name="Hemlock's Cat", - discriminator=7777, avatar_hash=None + id=9876, + name="Hemlock's Cat", + discriminator=7777, ), reason="He purrrrs like the best!", ) @@ -53,15 +55,17 @@ class StringDunderMethodTests(SimpleTestCase): DeletedMessage( id=45, author=User( - id=444, name='bill', - discriminator=5, avatar_hash=None + id=444, + name='bill', + discriminator=5, ), channel_id=666, content="wooey", deletion_context=MessageDeletionContext( actor=User( - id=5555, name='shawn', - discriminator=555, avatar_hash=None + id=5555, + name='shawn', + discriminator=555, ), creation=dt.utcnow() ), @@ -84,8 +88,9 @@ class StringDunderMethodTests(SimpleTestCase): Message( id=45, author=User( - id=444, name='bill', - discriminator=5, avatar_hash=None + id=444, + name='bill', + discriminator=5, ), channel_id=666, content="wooey", @@ -93,8 +98,9 @@ class StringDunderMethodTests(SimpleTestCase): ), MessageDeletionContext( actor=User( - id=5555, name='shawn', - discriminator=555, avatar_hash=None + id=5555, + name='shawn', + discriminator=555, ), creation=dt.utcnow() ), @@ -103,22 +109,29 @@ class StringDunderMethodTests(SimpleTestCase): embed={'content': "the builder"} ), User( - id=5, name='bob', - discriminator=1, avatar_hash=None + id=5, + name='bob', + discriminator=1, ), Infraction( - user_id=5, actor_id=5, - type='kick', reason='He terk my jerb!' + user_id=5, + actor_id=5, + type='kick', + reason='He terk my jerb!' ), Infraction( - user_id=5, actor_id=5, hidden=True, - type='kick', reason='He terk my jerb!', + user_id=5, + actor_id=5, + hidden=True, + type='kick', + reason='He terk my jerb!', expires_at=dt(5018, 11, 20, 15, 52, tzinfo=timezone.utc) ), Reminder( author=User( - id=452, name='billy', - discriminator=5, avatar_hash=None + id=452, + name='billy', + discriminator=5, ), channel_id=555, jump_url=( diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py index 76cb4112..92c62c87 100644 --- a/pydis_site/apps/api/tests/test_nominations.py +++ b/pydis_site/apps/api/tests/test_nominations.py @@ -13,7 +13,6 @@ class CreationTests(APISubdomainTestCase): id=1234, name='joe dart', discriminator=1111, - avatar_hash=None ) def test_accepts_valid_data(self): @@ -190,7 +189,6 @@ class NominationTests(APISubdomainTestCase): id=1234, name='joe dart', discriminator=1111, - avatar_hash=None ) cls.active_nomination = Nomination.objects.create( diff --git a/pydis_site/apps/api/tests/test_reminders.py b/pydis_site/apps/api/tests/test_reminders.py index 3441e0cc..c7fa07c9 100644 --- a/pydis_site/apps/api/tests/test_reminders.py +++ b/pydis_site/apps/api/tests/test_reminders.py @@ -53,7 +53,6 @@ class ReminderCreationTests(APISubdomainTestCase): id=1234, name='Mermaid Man', discriminator=1234, - avatar_hash=None, ) def test_accepts_valid_data(self): @@ -86,7 +85,6 @@ class ReminderDeletionTests(APISubdomainTestCase): id=6789, name='Barnacle Boy', discriminator=6789, - avatar_hash=None, ) cls.reminder = Reminder.objects.create( @@ -118,7 +116,6 @@ class ReminderListTests(APISubdomainTestCase): id=6789, name='Patrick Star', discriminator=6789, - avatar_hash=None, ) cls.reminder_one = Reminder.objects.create( @@ -172,7 +169,6 @@ class ReminderUpdateTests(APISubdomainTestCase): id=666, name='Man Ray', discriminator=666, - avatar_hash=None, ) cls.reminder = Reminder.objects.create( diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 86799f19..4f563dc6 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -49,7 +49,6 @@ class CreationTests(APISubdomainTestCase): url = reverse('bot:user-list', host='api') data = { 'id': 42, - 'avatar_hash': "validavatarhashiswear", 'name': "Test", 'discriminator': 42, 'roles': [ @@ -63,7 +62,6 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.json(), data) user = User.objects.get(id=42) - self.assertEqual(user.avatar_hash, data['avatar_hash']) self.assertEqual(user.name, data['name']) self.assertEqual(user.discriminator, data['discriminator']) self.assertEqual(user.in_guild, data['in_guild']) @@ -73,7 +71,6 @@ class CreationTests(APISubdomainTestCase): data = [ { 'id': 5, - 'avatar_hash': "hahayes", 'name': "test man", 'discriminator': 42, 'roles': [ @@ -83,7 +80,6 @@ class CreationTests(APISubdomainTestCase): }, { 'id': 8, - 'avatar_hash': "maybenot", 'name': "another test man", 'discriminator': 555, 'roles': [], @@ -99,7 +95,6 @@ class CreationTests(APISubdomainTestCase): url = reverse('bot:user-list', host='api') data = { 'id': 5, - 'avatar_hash': "hahayes", 'name': "test man", 'discriminator': 42, 'roles': [ @@ -114,7 +109,6 @@ class CreationTests(APISubdomainTestCase): url = reverse('bot:user-list', host='api') data = { 'id': True, - 'avatar_hash': 1902831, 'discriminator': "totally!" } @@ -148,7 +142,6 @@ class UserModelTests(APISubdomainTestCase): ) cls.user_with_roles = User.objects.create( id=1, - avatar_hash="coolavatarhash", name="Test User with two roles", discriminator=1111, in_guild=True, @@ -157,7 +150,6 @@ class UserModelTests(APISubdomainTestCase): cls.user_without_roles = User.objects.create( id=2, - avatar_hash="coolavatarhash", name="Test User without roles", discriminator=2222, in_guild=True, diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index a407787e..8f5bccfa 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -17,7 +17,6 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): >>> [ ... { ... 'id': 409107086526644234, - ... 'avatar': "3ba3c1acce584c20b1e96fc04bbe80eb", ... 'name': "Python", ... 'discriminator': 4329, ... 'roles': [ @@ -39,7 +38,6 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): #### Response format >>> { ... 'id': 409107086526644234, - ... 'avatar': "3ba3c1acce584c20b1e96fc04bbe80eb", ... 'name': "Python", ... 'discriminator': 4329, ... 'roles': [ @@ -62,7 +60,6 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): #### Request body >>> { ... 'id': int, - ... 'avatar': str, ... 'name': str, ... 'discriminator': int, ... 'roles': List[int], @@ -83,7 +80,6 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): #### Request body >>> { ... 'id': int, - ... 'avatar': str, ... 'name': str, ... 'discriminator': int, ... 'roles': List[int], @@ -102,7 +98,6 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): #### Request body >>> { ... 'id': int, - ... 'avatar': str, ... 'name': str, ... 'discriminator': int, ... 'roles': List[int], diff --git a/pydis_site/apps/home/tests/test_signal_listener.py b/pydis_site/apps/home/tests/test_signal_listener.py index 66a67252..fb9a17db 100644 --- a/pydis_site/apps/home/tests/test_signal_listener.py +++ b/pydis_site/apps/home/tests/test_signal_listener.py @@ -81,14 +81,12 @@ class SignalListenerTests(TestCase): id=0, name="user", discriminator=0, - avatar_hash=None ) cls.discord_unmapped = DiscordUser.objects.create( id=2, name="unmapped", discriminator=0, - avatar_hash=None ) cls.discord_unmapped.roles.add(cls.unmapped_role) @@ -98,7 +96,6 @@ class SignalListenerTests(TestCase): id=3, name="not-in-guild", discriminator=0, - avatar_hash=None, in_guild=False ) @@ -106,7 +103,6 @@ class SignalListenerTests(TestCase): id=1, name="admin", discriminator=0, - avatar_hash=None ) cls.discord_admin.roles.set([cls.admin_role]) @@ -116,7 +112,6 @@ class SignalListenerTests(TestCase): id=4, name="admin", discriminator=0, - avatar_hash=None ) cls.discord_moderator.roles.set([cls.moderator_role]) diff --git a/pydis_site/apps/staff/tests/test_logs_view.py b/pydis_site/apps/staff/tests/test_logs_view.py index 1415c558..936d1ad5 100644 --- a/pydis_site/apps/staff/tests/test_logs_view.py +++ b/pydis_site/apps/staff/tests/test_logs_view.py @@ -21,7 +21,6 @@ class TestLogsView(TestCase): id=12345678901, name='Alan Turing', discriminator=1912, - avatar_hash=None ) cls.author.roles.add(cls.developers_role) -- cgit v1.2.3 From e321a25c05875e82073470429708b37849947b16 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 08:23:14 +0300 Subject: OT: Replace help text of `used` field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Leon Sandøy --- pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py | 2 +- pydis_site/apps/api/models/bot/off_topic_channel_name.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py index 74836d8c..1b838aec 100644 --- a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py +++ b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py @@ -13,6 +13,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='offtopicchannelname', name='used', - field=models.BooleanField(default=False, help_text='Show is channel already used as channel name in this round.'), + field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), ), ] diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 3345754d..413cbfae 100644 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -18,7 +18,7 @@ class OffTopicChannelName(ModelReprMixin, models.Model): used = models.BooleanField( default=False, - help_text="Show is channel already used as channel name in this round." + help_text="Whether or not this name has already been used during this rotation", ) def __str__(self): -- cgit v1.2.3 From 1e3501b2f27d7e09a873dc3348d196ac2edda68b Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 08:49:49 +0300 Subject: OT: Fix migrations nr conflict --- .../api/migrations/0051_offtopicchannelname_used.py | 18 ------------------ .../api/migrations/0052_offtopicchannelname_used.py | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 18 deletions(-) delete mode 100644 pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py create mode 100644 pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py deleted file mode 100644 index 1b838aec..00000000 --- a/pydis_site/apps/api/migrations/0051_offtopicchannelname_used.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.11 on 2020-03-30 10:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0050_remove_infractions_active_default_value'), - ] - - operations = [ - migrations.AddField( - model_name='offtopicchannelname', - name='used', - field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py new file mode 100644 index 00000000..dfdf3835 --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.11 on 2020-03-30 10:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_create_news_setting'), + ] + + operations = [ + migrations.AddField( + model_name='offtopicchannelname', + name='used', + field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), + ), + ] -- cgit v1.2.3 From 7d5e1f9c60007b230fecdb2b649c5574462fdeb1 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 09:06:21 +0300 Subject: OT: Refactor off-topic-names random items getting Remove `mark_used` parameter and move this functionality to `random_items` parameter. Update docstring of class --- .../api/viewsets/bot/off_topic_channel_name.py | 55 ++++++++++------------ 1 file changed, 25 insertions(+), 30 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 4328c894..e6cf8172 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -20,11 +20,9 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): Return all known off-topic channel names from the database. If the `random_items` query parameter is given, for example using... $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?random_items=5 - ... then the API will return `5` random items from the database. - If the `mark_used` query parameter is given like... - $ curl api.pydis.local:8000/bot/off-topic-channel-names?random_items=5&mark_used=true - ... then the API will mark returned `5` items `used`. - When running out of names, API will mark all names to not used and start new round. + ... then the API will return `5` random items from the database + that is not used in current rotation. + When running out of names, API will mark all names to not used and start new rotation. #### Response format Return a list of off-topic-channel names: @@ -110,32 +108,29 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) - if 'mark_used' in request.query_params and request.query_params['mark_used']: - queryset = self.get_queryset().order_by('?').exclude(used=True)[:random_count] - self.get_queryset().filter( + queryset = self.get_queryset().order_by('?').exclude(used=True)[:random_count] + self.get_queryset().filter( + name__in=(query.name for query in queryset) + ).update(used=True) + + # When client request more channel names than non-used names is available, start + # new round of names. + if len(queryset) < random_count: + # Get how much names still missing and don't fetch duplicate names. + need_more = random_count - len(queryset) + ext = self.get_queryset().order_by('?').exclude( name__in=(query.name for query in queryset) - ).update(used=True) - - # When client request more channel names than non-used names is available, start - # new round of names. - if len(queryset) < random_count: - # Get how much names still missing and don't fetch duplicate names. - need_more = random_count - len(queryset) - ext = self.get_queryset().order_by('?').exclude( - name__in=(query.name for query in queryset) - )[:need_more] - - # Set all names `used` field to False except these that we just used. - self.get_queryset().exclude(name__in=( - query.name for query in ext) - ).update(used=False) - # Join original queryset (that had missing names) - # and extension with these missing names. - queryset = list(queryset) + list(ext) - serialized = self.serializer_class(queryset, many=True) - return Response(serialized.data) - - queryset = self.get_queryset().order_by('?')[:random_count] + )[:need_more] + + # Set all names `used` field to False except these that we just used. + self.get_queryset().exclude(name__in=( + query.name for query in ext) + ).update(used=False) + + # Join original queryset (that had missing names) + # and extension with these missing names. + queryset = list(queryset) + list(ext) + serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From 8584aa2a409922c60b9c4a8e0874bc92cd01b9a1 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 09:12:03 +0300 Subject: OT Tests: Refactor tests to latest change --- .../apps/api/tests/test_off_topic_channel_names.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index 06624d89..51372651 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -21,12 +21,6 @@ class UnauthenticatedTests(APISubdomainTestCase): self.assertEqual(response.status_code, 401) - def test_cannot_read_off_topic_channel_name_list_with_random_item_and_mark_used_param(self): - url = reverse('bot:offtopicchannelname-list', host='api') - response = self.client.get(f'{url}?random_items=no&mark_used=true') - - self.assertEqual(response.status_code, 401) - class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_empty_object(self): @@ -87,17 +81,11 @@ class ListTests(APISubdomainTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 1) - - def test_returns_single_correct_item_with_mark_used_parameter_true_and_random_items_1(self): - url = reverse('bot:offtopicchannelname-list', host='api') - response = self.client.get(f'{url}?random_items=1&mark_used=true') - - self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), [self.test_name.name]) - def test_running_out_of_names_with_mark_used_parameter(self): + def test_running_out_of_names_with_random_parameter(self): url = reverse('bot:offtopicchannelname-list', host='api') - response = self.client.get(f'{url}?random_items=2&mark_used=true') + response = self.client.get(f'{url}?random_items=2') self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name]) -- cgit v1.2.3 From 72e3ee0645d8017bfdac27f9fd876137164f3e3e Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 09:30:05 +0300 Subject: OT Tests: Add docstring to tests --- pydis_site/apps/api/tests/test_off_topic_channel_names.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index 51372651..cac9405a 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -10,12 +10,14 @@ class UnauthenticatedTests(APISubdomainTestCase): self.client.force_authenticate(user=None) def test_cannot_read_off_topic_channel_name_list(self): + """Test does this return 401 response code when not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_cannot_read_off_topic_channel_name_list_with_random_item_param(self): + """Test does this give 401 code when `random_items` provided and not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=no') @@ -24,6 +26,7 @@ class UnauthenticatedTests(APISubdomainTestCase): class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_empty_object(self): + """Test does this return empty list when no names in database.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -31,6 +34,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_empty_list_with_get_all_param(self): + """Test does this return empty list when no names and `random_items` param provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=5') @@ -38,6 +42,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_400_for_bad_random_items_param(self): + """Test does this return error message when passing not integer as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=totally-a-valid-integer') @@ -47,6 +52,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): }) def test_returns_400_for_negative_random_items_param(self): + """Test does this return error message when passing negative int as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=-5') @@ -63,6 +69,7 @@ class ListTests(APISubdomainTestCase): cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True) def test_returns_name_in_list(self): + """Test does this return all off-topic channel names.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -76,6 +83,7 @@ class ListTests(APISubdomainTestCase): ) def test_returns_single_item_with_random_items_param_set_to_1(self): + """Test does this return not-used name instead used.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=1') @@ -84,6 +92,7 @@ class ListTests(APISubdomainTestCase): self.assertEqual(response.json(), [self.test_name.name]) def test_running_out_of_names_with_random_parameter(self): + """Test does this reset names `used` parameter to `False` when running out of names.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=2') @@ -101,6 +110,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_201_for_unicode_chars(self): + """Test does this accept all valid characters.""" url = reverse('bot:offtopicchannelname-list', host='api') names = ( '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹', @@ -112,6 +122,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_400_for_missing_name_param(self): + """Test does this return error message when name not provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.post(url) self.assertEqual(response.status_code, 400) @@ -120,6 +131,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_bad_name_param(self): + """Test does this return error message when invalid characters provided.""" url = reverse('bot:offtopicchannelname-list', host='api') invalid_names = ( 'space between words', @@ -142,18 +154,21 @@ class DeletionTests(APISubdomainTestCase): cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') def test_deleting_unknown_name_returns_404(self): + """Test does this return 404 code when trying to delete unknown name.""" url = reverse('bot:offtopicchannelname-detail', args=('unknown-name',), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 404) def test_deleting_known_name_returns_204(self): + """Test does this return 204 code when deleting was successful.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name.name,), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 204) def test_name_gets_deleted(self): + """Test does name gets actually deleted.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api') response = self.client.delete(url) -- cgit v1.2.3 From 5c53af373f9f868c5603a903a2c2d9636c2d0982 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 09:35:17 +0300 Subject: OT: Fix comments --- pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index e6cf8172..c4520a48 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -113,16 +113,16 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): name__in=(query.name for query in queryset) ).update(used=True) - # When client request more channel names than non-used names is available, start - # new round of names. + # When the client requests more channel names than are available, + # we reset all names to used=False and start a new round of names. if len(queryset) < random_count: - # Get how much names still missing and don't fetch duplicate names. + # Figure out how many additional names we need, and don't fetch duplicate names. need_more = random_count - len(queryset) ext = self.get_queryset().order_by('?').exclude( name__in=(query.name for query in queryset) )[:need_more] - # Set all names `used` field to False except these that we just used. + # Reset the `used` field to False for all names except the ones we just used. self.get_queryset().exclude(name__in=( query.name for query in ext) ).update(used=False) -- cgit v1.2.3 From e046ac1d764d80a6a04c3c2d70fb8b4d718b6210 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 09:36:24 +0300 Subject: OT: Rename variable `need_more` to `names_needed` --- pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index c4520a48..1099922c 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -117,10 +117,10 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): # we reset all names to used=False and start a new round of names. if len(queryset) < random_count: # Figure out how many additional names we need, and don't fetch duplicate names. - need_more = random_count - len(queryset) + names_needed = random_count - len(queryset) ext = self.get_queryset().order_by('?').exclude( name__in=(query.name for query in queryset) - )[:need_more] + )[:names_needed] # Reset the `used` field to False for all names except the ones we just used. self.get_queryset().exclude(name__in=( -- cgit v1.2.3 From 9cedba3870947ca10218c65c4106f5dd095d230c Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 28 May 2020 09:37:29 +0300 Subject: OT: Rename variable `ext` to `other_names` --- pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 1099922c..9af69ae4 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -118,18 +118,18 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): if len(queryset) < random_count: # Figure out how many additional names we need, and don't fetch duplicate names. names_needed = random_count - len(queryset) - ext = self.get_queryset().order_by('?').exclude( + other_names = self.get_queryset().order_by('?').exclude( name__in=(query.name for query in queryset) )[:names_needed] # Reset the `used` field to False for all names except the ones we just used. self.get_queryset().exclude(name__in=( - query.name for query in ext) + query.name for query in other_names) ).update(used=False) # Join original queryset (that had missing names) # and extension with these missing names. - queryset = list(queryset) + list(ext) + queryset = list(queryset) + list(other_names) serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From 0571baa63c039be061ddf448a8f0b0d3b24ea32e Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Tue, 2 Jun 2020 21:12:01 +0100 Subject: Convert the roles field on the user model from a many-to-many field to a postgres array with roles --- pydis_site/apps/api/models/bot/user.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index 65e8751e..cc4e7873 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -1,10 +1,22 @@ +from django.contrib.postgres.fields import ArrayField from django.core.validators import MaxValueValidator, MinValueValidator +from django.core.exceptions import ValidationError from django.db import models from pydis_site.apps.api.models.bot.role import Role from pydis_site.apps.api.models.utils import ModelReprMixin +def _validate_existing_role(value: int) -> None: + """Validate that a role exists when given in to the user model.""" + role = Role.objects.filter(id=value) + + if not role: + raise ValidationError( + f"Role with ID {value} does not exist" + ) + + class User(ModelReprMixin, models.Model): """A Discord user.""" @@ -31,9 +43,18 @@ class User(ModelReprMixin, models.Model): ), help_text="The discriminator of this user, taken from Discord." ) - roles = models.ManyToManyField( - Role, - help_text="Any roles this user has on our server." + roles = ArrayField( + models.BigIntegerField( + validators=( + MinValueValidator( + limit_value=0, + message="Role IDs cannot be negative." + ), + _validate_existing_role + ) + ), + default=list, + help_text="IDs of roles the user has on the server" ) in_guild = models.BooleanField( default=True, @@ -51,7 +72,7 @@ class User(ModelReprMixin, models.Model): This will fall back to the Developers role if the user does not have any roles. """ - roles = self.roles.all() + roles = Role.objects.filter(id__in=self.roles) if not roles: return Role.objects.get(name="Developers") - return max(self.roles.all()) + return max(roles) -- cgit v1.2.3 From fac0d15afade7b00e883e3f9798a846956f222cc Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Tue, 2 Jun 2020 21:13:01 +0100 Subject: Add migrations to switch user field to array --- .../api/migrations/0053_user_roles_to_array.py | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0053_user_roles_to_array.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py new file mode 100644 index 00000000..7ff3a548 --- /dev/null +++ b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.11 on 2020-06-02 13:42 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0052_remove_user_avatar_hash'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='roles', + ), + migrations.AddField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] -- cgit v1.2.3 From 044fb2661de97c710c37e48f908d99a4b173a264 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Tue, 2 Jun 2020 21:13:19 +0100 Subject: Add validator to ensure roles passed to user model exist --- .../migrations/0054_user_invalidate_unknown_role.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py new file mode 100644 index 00000000..96230015 --- /dev/null +++ b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.11 on 2020-06-02 20:08 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0053_user_roles_to_array'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] -- cgit v1.2.3 From 967ac5cb20280d4b3d70e9c9d15b435475cd70df Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Tue, 2 Jun 2020 21:13:30 +0100 Subject: Remove primary key from user serializer --- pydis_site/apps/api/serializers.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index cc3f167d..f2d5144c 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -229,8 +229,6 @@ class TagSerializer(ModelSerializer): class UserSerializer(BulkSerializerMixin, ModelSerializer): """A class providing (de-)serialization of `User` instances.""" - roles = PrimaryKeyRelatedField(many=True, queryset=Role.objects.all(), required=False) - class Meta: """Metadata defined for the Django REST Framework.""" -- cgit v1.2.3 From 9c439d61b46dc37dd6b4fb4f5daa25aa671139e8 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Tue, 2 Jun 2020 21:13:46 +0100 Subject: Alter signals to handle OAuth2 with groups and role mappings --- pydis_site/apps/home/signals.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/home/signals.py b/pydis_site/apps/home/signals.py index 4cb4564b..8af48c15 100644 --- a/pydis_site/apps/home/signals.py +++ b/pydis_site/apps/home/signals.py @@ -150,7 +150,7 @@ class AllauthSignalListener: social_account = SocialAccount.objects.get(user=user, provider=DiscordProvider.id) discord_user = DiscordUser.objects.get(id=int(social_account.uid)) - mappings = RoleMapping.objects.filter(role__in=discord_user.roles.all()).all() + mappings = RoleMapping.objects.filter(role__id__in=discord_user.roles).all() is_staff = any(m.is_staff for m in mappings) if user.is_staff != is_staff: @@ -185,7 +185,7 @@ class AllauthSignalListener: self.mapping_model_deleted(RoleMapping, instance=old_instance) accounts = SocialAccount.objects.filter( - uid__in=(u.id for u in instance.role.user_set.all()) + uid__in=(u.id for u in DiscordUser.objects.filter(roles__contains=[instance.role.id])) ) for account in accounts: @@ -198,7 +198,7 @@ class AllauthSignalListener: discord_user = DiscordUser.objects.get(id=int(account.uid)) mappings = RoleMapping.objects.filter( - role__in=discord_user.roles.all() + role__id__in=discord_user.roles ).exclude(id=instance.id).all() is_staff = any(m.is_staff for m in mappings) @@ -289,9 +289,9 @@ class AllauthSignalListener: new_groups = [] is_staff = False - for role in user.roles.all(): + for role in user.roles: try: - mapping = mappings.get(role=role) + mapping = mappings.get(role__id=role) except RoleMapping.DoesNotExist: continue # No mapping exists -- cgit v1.2.3 From a32c9f1a7c5e2a84627c87b049a43c281fa26b5b Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Tue, 2 Jun 2020 21:14:11 +0100 Subject: Alter account utilities to check list length instead of queryset length --- pydis_site/utils/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/utils/account.py b/pydis_site/utils/account.py index 2d699c88..b4e41198 100644 --- a/pydis_site/utils/account.py +++ b/pydis_site/utils/account.py @@ -54,7 +54,7 @@ class SocialAccountAdapter(DefaultSocialAccountAdapter): raise ImmediateHttpResponse(redirect(reverse("home"))) - if user.roles.count() <= 1: + if len(user.roles) <= 1: add_message(request, ERROR, ERROR_JOIN_DISCORD) raise ImmediateHttpResponse(redirect(reverse("home"))) -- cgit v1.2.3 From 3cbe2b173db184d655939fd6d83fee4870b7da49 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Tue, 2 Jun 2020 21:14:28 +0100 Subject: Alter API tests to use new user roles format --- pydis_site/apps/api/tests/test_users.py | 2 +- pydis_site/apps/api/viewsets/bot/user.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_users.py b/pydis_site/apps/api/tests/test_users.py index 4f563dc6..4c0f6e27 100644 --- a/pydis_site/apps/api/tests/test_users.py +++ b/pydis_site/apps/api/tests/test_users.py @@ -146,7 +146,7 @@ class UserModelTests(APISubdomainTestCase): discriminator=1111, in_guild=True, ) - cls.user_with_roles.roles.add(cls.role_bottom, cls.role_top) + cls.user_with_roles.roles.extend([cls.role_bottom.id, cls.role_top.id]) cls.user_without_roles = User.objects.create( id=2, diff --git a/pydis_site/apps/api/viewsets/bot/user.py b/pydis_site/apps/api/viewsets/bot/user.py index 8f5bccfa..9571b3d7 100644 --- a/pydis_site/apps/api/viewsets/bot/user.py +++ b/pydis_site/apps/api/viewsets/bot/user.py @@ -118,4 +118,4 @@ class UserViewSet(BulkCreateModelMixin, ModelViewSet): """ serializer_class = UserSerializer - queryset = User.objects.prefetch_related('roles') + queryset = User.objects -- cgit v1.2.3 From 89437db9dc943d03799f65281e9c97dd6ff9af7f Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Tue, 2 Jun 2020 21:15:08 +0100 Subject: Alter account utils test to use new user roles format --- pydis_site/tests/test_utils_account.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/tests/test_utils_account.py b/pydis_site/tests/test_utils_account.py index d2946368..e8db7b64 100644 --- a/pydis_site/tests/test_utils_account.py +++ b/pydis_site/tests/test_utils_account.py @@ -61,16 +61,18 @@ class AccountUtilsTests(TestCase): position=0 ) - self.discord_user_role.roles.add(everyone_role) - self.discord_user_two_roles.roles.add(everyone_role) + self.discord_user_role.roles.append(everyone_role.id) + self.discord_user_two_roles.roles.append(everyone_role.id) - self.discord_user_two_roles.roles.add(Role.objects.create( + developers_role = Role.objects.create( id=1, name="Developers", colour=0, permissions=0, position=1 - )) + ) + + self.discord_user_two_roles.roles.append(developers_role.id) def test_account_adapter(self): """Test that our Allauth account adapter functions correctly.""" -- cgit v1.2.3 From 3bb6ad7eb3ae6b24e8dc13e2458cc3a38b533d7f Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Tue, 2 Jun 2020 21:15:20 +0100 Subject: Alter staff tests to make use of new user roles syntax --- pydis_site/apps/staff/tests/test_logs_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/staff/tests/test_logs_view.py b/pydis_site/apps/staff/tests/test_logs_view.py index 936d1ad5..17910bb6 100644 --- a/pydis_site/apps/staff/tests/test_logs_view.py +++ b/pydis_site/apps/staff/tests/test_logs_view.py @@ -23,7 +23,7 @@ class TestLogsView(TestCase): discriminator=1912, ) - cls.author.roles.add(cls.developers_role) + cls.author.roles.append(cls.developers_role.id) cls.deletion_context = MessageDeletionContext.objects.create( actor=cls.actor, -- cgit v1.2.3 From 034a0886b61a61b5ef90ca4f28d56a297b0af11e Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Tue, 2 Jun 2020 21:15:30 +0100 Subject: Alter signal tests to make use of new user roles syntax --- pydis_site/apps/home/tests/test_signal_listener.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/home/tests/test_signal_listener.py b/pydis_site/apps/home/tests/test_signal_listener.py index fb9a17db..d99d81a5 100644 --- a/pydis_site/apps/home/tests/test_signal_listener.py +++ b/pydis_site/apps/home/tests/test_signal_listener.py @@ -89,7 +89,7 @@ class SignalListenerTests(TestCase): discriminator=0, ) - cls.discord_unmapped.roles.add(cls.unmapped_role) + cls.discord_unmapped.roles.append(cls.unmapped_role.id) cls.discord_unmapped.save() cls.discord_not_in_guild = DiscordUser.objects.create( @@ -105,7 +105,7 @@ class SignalListenerTests(TestCase): discriminator=0, ) - cls.discord_admin.roles.set([cls.admin_role]) + cls.discord_admin.roles = [cls.admin_role.id] cls.discord_admin.save() cls.discord_moderator = DiscordUser.objects.create( @@ -114,7 +114,7 @@ class SignalListenerTests(TestCase): discriminator=0, ) - cls.discord_moderator.roles.set([cls.moderator_role]) + cls.discord_moderator.roles = [cls.moderator_role.id] cls.discord_moderator.save() cls.django_user_discordless = DjangoUser.objects.create(username="no-discord") @@ -333,7 +333,7 @@ class SignalListenerTests(TestCase): handler._apply_groups(self.discord_admin, self.social_admin) self.assertEqual(self.django_user_discordless.groups.all().count(), 0) - self.discord_admin.roles.add(self.admin_role) + self.discord_admin.roles.append(self.admin_role.id) self.discord_admin.save() def test_apply_groups_moderator(self): @@ -360,7 +360,7 @@ class SignalListenerTests(TestCase): handler._apply_groups(self.discord_moderator, self.social_moderator) self.assertEqual(self.django_user_discordless.groups.all().count(), 0) - self.discord_moderator.roles.add(self.moderator_role) + self.discord_moderator.roles.append(self.moderator_role.id) self.discord_moderator.save() def test_apply_groups_other(self): -- cgit v1.2.3 From f992f4b2900716ce5085aae1cb6fd104b0e6fd38 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Tue, 2 Jun 2020 21:20:58 +0100 Subject: Alter import order in user model --- pydis_site/apps/api/models/bot/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index cc4e7873..c7510d8c 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -1,6 +1,6 @@ from django.contrib.postgres.fields import ArrayField -from django.core.validators import MaxValueValidator, MinValueValidator from django.core.exceptions import ValidationError +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from pydis_site.apps.api.models.bot.role import Role -- cgit v1.2.3 From 2a02c68c0b05b307fdf48341b06a2ea2f6755fd3 Mon Sep 17 00:00:00 2001 From: Dennis Pham Date: Thu, 4 Jun 2020 21:07:16 -0400 Subject: Update navbar for Code Jam 7 Changed `Most Recent: Game Jam 2020` to `Upcoming: Code Jam 7` --- pydis_site/templates/base/navbar.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html index d2ea9589..f07662ae 100644 --- a/pydis_site/templates/base/navbar.html +++ b/pydis_site/templates/base/navbar.html @@ -87,8 +87,8 @@ - - Most Recent: Game Jam 2020 + + Upcoming: Code Jam 7 All events -- cgit v1.2.3 From 56fabe78d27286b431d391cf63e1adabdb8787a0 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Fri, 5 Jun 2020 12:48:06 +0100 Subject: Remove very generous newline in role validator Co-authored-by: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> --- pydis_site/apps/api/models/bot/user.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index c7510d8c..bff4d642 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -12,9 +12,7 @@ def _validate_existing_role(value: int) -> None: role = Role.objects.filter(id=value) if not role: - raise ValidationError( - f"Role with ID {value} does not exist" - ) + raise ValidationError(f"Role with ID {value} does not exist") class User(ModelReprMixin, models.Model): -- cgit v1.2.3 From 7048cf15868f234cd59fe6072e1b77a81d3140a0 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 7 Jun 2020 01:49:04 +0200 Subject: Add Summer Code Jam image on landing page. --- .../static/images/events/summer_code_jam_2020.png | Bin 0 -> 271282 bytes pydis_site/templates/home/index.html | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 pydis_site/static/images/events/summer_code_jam_2020.png (limited to 'pydis_site') diff --git a/pydis_site/static/images/events/summer_code_jam_2020.png b/pydis_site/static/images/events/summer_code_jam_2020.png new file mode 100644 index 00000000..63c311b0 Binary files /dev/null and b/pydis_site/static/images/events/summer_code_jam_2020.png differ diff --git a/pydis_site/templates/home/index.html b/pydis_site/templates/home/index.html index c30cbee6..3e96cc91 100644 --- a/pydis_site/templates/home/index.html +++ b/pydis_site/templates/home/index.html @@ -39,8 +39,8 @@ {# Right column container #} -- cgit v1.2.3 From 228a56ce1b61a2bf8750228779fde7347f835873 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 7 Jun 2020 22:17:06 +0200 Subject: Replace django-crispy-bulma -> flake8-annotations --- pydis_site/apps/home/views/home.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/home/views/home.py b/pydis_site/apps/home/views/home.py index 4cf22594..79c7695b 100644 --- a/pydis_site/apps/home/views/home.py +++ b/pydis_site/apps/home/views/home.py @@ -23,8 +23,8 @@ class HomeView(View): "python-discord/bot", "python-discord/snekbox", "python-discord/seasonalbot", + "python-discord/flake8-annotations", "python-discord/django-simple-bulma", - "python-discord/django-crispy-bulma", ] def _get_api_data(self) -> Dict[str, Dict[str, str]]: -- cgit v1.2.3 From 27a27e5be8d873b8cbdc5f335dc38a627e4732be Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 7 Jun 2020 22:41:02 +0200 Subject: Fix broken github api test data --- pydis_site/apps/home/tests/mock_github_api_response.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/home/tests/mock_github_api_response.json b/pydis_site/apps/home/tests/mock_github_api_response.json index 37dc672e..35604a85 100644 --- a/pydis_site/apps/home/tests/mock_github_api_response.json +++ b/pydis_site/apps/home/tests/mock_github_api_response.json @@ -28,7 +28,7 @@ "forks_count": 31 }, { - "full_name": "python-discord/django-crispy-bulma", + "full_name": "python-discord/flake8-annotations", "description": "test", "stargazers_count": 97, "language": "Python", -- cgit v1.2.3 From 61ea5ad7439b43d80d6e2184fd663924a5a7dcc7 Mon Sep 17 00:00:00 2001 From: Dennis Pham Date: Mon, 22 Jun 2020 16:10:25 -0400 Subject: Catch ConnectionError when trying to get updated repository data This will prevent a 500 server error on our homepage when GitHub's API is down, allowing us to use the cached data we have instead. --- pydis_site/apps/home/views/home.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/home/views/home.py b/pydis_site/apps/home/views/home.py index 79c7695b..20e38ab0 100644 --- a/pydis_site/apps/home/views/home.py +++ b/pydis_site/apps/home/views/home.py @@ -61,7 +61,7 @@ class HomeView(View): # Try to get new data from the API. If it fails, return the cached data. try: api_repositories = self._get_api_data() - except TypeError: + except (TypeError, ConnectionError): return RepositoryMetadata.objects.all() database_repositories = [] -- cgit v1.2.3 From 1f4beeb10ccec010aa2d503ed73b4b64e9c1895f Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Tue, 14 Jul 2020 14:21:16 +0200 Subject: Rename utils.py to mixins.py. More precise. https://github.com/python-discord/site/issues/305 --- pydis_site/apps/api/migrations/0049_offensivemessage.py | 4 ++-- pydis_site/apps/api/models/__init__.py | 2 +- pydis_site/apps/api/models/bot/bot_setting.py | 2 +- pydis_site/apps/api/models/bot/documentation_link.py | 2 +- pydis_site/apps/api/models/bot/infraction.py | 2 +- pydis_site/apps/api/models/bot/message.py | 2 +- .../apps/api/models/bot/message_deletion_context.py | 2 +- pydis_site/apps/api/models/bot/nomination.py | 2 +- .../apps/api/models/bot/off_topic_channel_name.py | 2 +- pydis_site/apps/api/models/bot/offensive_message.py | 2 +- pydis_site/apps/api/models/bot/reminder.py | 2 +- pydis_site/apps/api/models/bot/role.py | 2 +- pydis_site/apps/api/models/bot/tag.py | 2 +- pydis_site/apps/api/models/bot/user.py | 2 +- pydis_site/apps/api/models/log_entry.py | 2 +- pydis_site/apps/api/models/mixins.py | 17 +++++++++++++++++ pydis_site/apps/api/models/utils.py | 17 ----------------- 17 files changed, 33 insertions(+), 33 deletions(-) create mode 100644 pydis_site/apps/api/models/mixins.py delete mode 100644 pydis_site/apps/api/models/utils.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0049_offensivemessage.py b/pydis_site/apps/api/migrations/0049_offensivemessage.py index fe4a1961..f342cec3 100644 --- a/pydis_site/apps/api/migrations/0049_offensivemessage.py +++ b/pydis_site/apps/api/migrations/0049_offensivemessage.py @@ -3,7 +3,7 @@ import django.core.validators from django.db import migrations, models import pydis_site.apps.api.models.bot.offensive_message -import pydis_site.apps.api.models.utils +import pydis_site.apps.api.models.mixins class Migration(migrations.Migration): @@ -20,6 +20,6 @@ class Migration(migrations.Migration): ('channel_id', models.BigIntegerField(help_text='The channel ID that the message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')])), ('delete_date', models.DateTimeField(help_text='The date on which the message will be auto-deleted.', validators=[pydis_site.apps.api.models.bot.offensive_message.future_date_validator])), ], - bases=(pydis_site.apps.api.models.utils.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index 450d18cd..644b8757 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -15,4 +15,4 @@ from .bot import ( User ) from .log_entry import LogEntry -from .utils import ModelReprMixin +from .mixins import ModelReprMixin diff --git a/pydis_site/apps/api/models/bot/bot_setting.py b/pydis_site/apps/api/models/bot/bot_setting.py index 8d48eac7..2a3944f8 100644 --- a/pydis_site/apps/api/models/bot/bot_setting.py +++ b/pydis_site/apps/api/models/bot/bot_setting.py @@ -2,7 +2,7 @@ from django.contrib.postgres import fields as pgfields from django.core.exceptions import ValidationError from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin def validate_bot_setting_name(name: str) -> None: diff --git a/pydis_site/apps/api/models/bot/documentation_link.py b/pydis_site/apps/api/models/bot/documentation_link.py index f844ae04..5a46460b 100644 --- a/pydis_site/apps/api/models/bot/documentation_link.py +++ b/pydis_site/apps/api/models/bot/documentation_link.py @@ -1,6 +1,6 @@ from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class DocumentationLink(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/infraction.py b/pydis_site/apps/api/models/bot/infraction.py index f58e89a3..7660cbba 100644 --- a/pydis_site/apps/api/models/bot/infraction.py +++ b/pydis_site/apps/api/models/bot/infraction.py @@ -2,7 +2,7 @@ from django.db import models from django.utils import timezone from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class Infraction(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index 8b18fc9f..7694ac75 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -7,7 +7,7 @@ from django.utils import timezone from pydis_site.apps.api.models.bot.tag import validate_tag_embed from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class Message(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/message_deletion_context.py b/pydis_site/apps/api/models/bot/message_deletion_context.py index 44a0c8ae..04ae8d34 100644 --- a/pydis_site/apps/api/models/bot/message_deletion_context.py +++ b/pydis_site/apps/api/models/bot/message_deletion_context.py @@ -1,7 +1,7 @@ from django.db import models from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class MessageDeletionContext(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index cd9951aa..21e34e87 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -1,7 +1,7 @@ from django.db import models from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class Nomination(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 29280c27..20e77b9f 100644 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -1,7 +1,7 @@ from django.core.validators import RegexValidator from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class OffTopicChannelName(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/offensive_message.py b/pydis_site/apps/api/models/bot/offensive_message.py index b466d9c2..6c0e5ffb 100644 --- a/pydis_site/apps/api/models/bot/offensive_message.py +++ b/pydis_site/apps/api/models/bot/offensive_message.py @@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin def future_date_validator(date: datetime.date) -> None: diff --git a/pydis_site/apps/api/models/bot/reminder.py b/pydis_site/apps/api/models/bot/reminder.py index d53fedb5..28722435 100644 --- a/pydis_site/apps/api/models/bot/reminder.py +++ b/pydis_site/apps/api/models/bot/reminder.py @@ -2,7 +2,7 @@ from django.core.validators import MinValueValidator from django.db import models from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class Reminder(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/role.py b/pydis_site/apps/api/models/bot/role.py index 58bbf8b4..721e4815 100644 --- a/pydis_site/apps/api/models/bot/role.py +++ b/pydis_site/apps/api/models/bot/role.py @@ -3,7 +3,7 @@ from __future__ import annotations from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class Role(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/bot/tag.py b/pydis_site/apps/api/models/bot/tag.py index 5d4cc393..5e53582f 100644 --- a/pydis_site/apps/api/models/bot/tag.py +++ b/pydis_site/apps/api/models/bot/tag.py @@ -6,7 +6,7 @@ from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, MinLengthValidator from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin def is_bool_validator(value: Any) -> None: diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index bff4d642..d7f203aa 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -4,7 +4,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from pydis_site.apps.api.models.bot.role import Role -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin def _validate_existing_role(value: int) -> None: diff --git a/pydis_site/apps/api/models/log_entry.py b/pydis_site/apps/api/models/log_entry.py index 488af48e..752cd2ca 100644 --- a/pydis_site/apps/api/models/log_entry.py +++ b/pydis_site/apps/api/models/log_entry.py @@ -1,7 +1,7 @@ from django.db import models from django.utils import timezone -from pydis_site.apps.api.models.utils import ModelReprMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin class LogEntry(ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py new file mode 100644 index 00000000..0540c4de --- /dev/null +++ b/pydis_site/apps/api/models/mixins.py @@ -0,0 +1,17 @@ +from operator import itemgetter + + +class ModelReprMixin: + """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" + + def __repr__(self): + """Returns the current model class name and initialisation parameters.""" + attributes = ' '.join( + f'{attribute}={value!r}' + for attribute, value in sorted( + self.__dict__.items(), + key=itemgetter(0) + ) + if not attribute.startswith('_') + ) + return f'<{self.__class__.__name__}({attributes})>' diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py deleted file mode 100644 index 0540c4de..00000000 --- a/pydis_site/apps/api/models/utils.py +++ /dev/null @@ -1,17 +0,0 @@ -from operator import itemgetter - - -class ModelReprMixin: - """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" - - def __repr__(self): - """Returns the current model class name and initialisation parameters.""" - attributes = ' '.join( - f'{attribute}={value!r}' - for attribute, value in sorted( - self.__dict__.items(), - key=itemgetter(0) - ) - if not attribute.startswith('_') - ) - return f'<{self.__class__.__name__}({attributes})>' -- cgit v1.2.3 From a9fb4ac8213c8131a8a6f7f339d2ba6b341e6cdb Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Tue, 14 Jul 2020 14:29:27 +0200 Subject: Add a mixin for adding created and updated times. https://github.com/python-discord/site/issues/305 --- pydis_site/apps/api/models/__init__.py | 2 +- pydis_site/apps/api/models/mixins.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index 644b8757..1c9e1d07 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -15,4 +15,4 @@ from .bot import ( User ) from .log_entry import LogEntry -from .mixins import ModelReprMixin +from .mixins import ModelReprMixin, ModelTimestampMixin diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py index 0540c4de..942edaa1 100644 --- a/pydis_site/apps/api/models/mixins.py +++ b/pydis_site/apps/api/models/mixins.py @@ -1,5 +1,7 @@ from operator import itemgetter +from django.db import models + class ModelReprMixin: """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" @@ -15,3 +17,13 @@ class ModelReprMixin: if not attribute.startswith('_') ) return f'<{self.__class__.__name__}({attributes})>' + + +class ModelTimestampMixin(models.Model): + """Mixin providing created_at and updated_at fields.""" + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True -- cgit v1.2.3 From ee743f622dd0ba6f8c0b9801b1db1d85d60fa697 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Tue, 14 Jul 2020 15:48:34 +0200 Subject: Add the AllowList model and serializer. This is the model which we will use for items that are either blacklisted or whitelisted. https://github.com/python-discord/site/issues/305 --- pydis_site/apps/api/models/__init__.py | 1 + pydis_site/apps/api/models/bot/__init__.py | 1 + pydis_site/apps/api/models/bot/allowlist.py | 28 ++++++++++++++++++++++++++++ pydis_site/apps/api/serializers.py | 12 +++++++++++- 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 pydis_site/apps/api/models/bot/allowlist.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index 1c9e1d07..04d0fc50 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa from .bot import ( + AllowList, BotSetting, DocumentationLink, DeletedMessage, diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index 8ae47746..b373ee84 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -1,4 +1,5 @@ # flake8: noqa +from .allowlist import AllowList from .bot_setting import BotSetting from .deleted_message import DeletedMessage from .documentation_link import DocumentationLink diff --git a/pydis_site/apps/api/models/bot/allowlist.py b/pydis_site/apps/api/models/bot/allowlist.py new file mode 100644 index 00000000..c8fa2e33 --- /dev/null +++ b/pydis_site/apps/api/models/bot/allowlist.py @@ -0,0 +1,28 @@ +from django.db import models + +from pydis_site.apps.api.models import ModelReprMixin, ModelTimestampMixin + + +class AllowList(ModelTimestampMixin, ModelReprMixin, models.Model): + """An item that is either allowed or denied.""" + + AllowListType = models.TextChoices( + 'guild_invite_id', + 'file_format', + 'domain_name', + 'word_watchlist', + ) + type = models.CharField( + max_length=50, + help_text=( + "The type of allowlist this is on. The value must be one of the following: " + f"{','.join(AllowListType.choices)}." + ), + choices=AllowListType.choices, + ) + allowed = models.BooleanField( + help_text="Whether this item is on the allowlist or the denylist." + ) + content = models.TextField( + help_text="The data to add to the allowlist." + ) diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index f2d5144c..24ba0ec0 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -8,7 +8,7 @@ from .models import ( DocumentationLink, Infraction, LogEntry, MessageDeletionContext, Nomination, OffTopicChannelName, - OffensiveMessage, + OffensiveMessage, AllowList, Reminder, Role, Tag, User ) @@ -97,6 +97,16 @@ class DocumentationLinkSerializer(ModelSerializer): fields = ('package', 'base_url', 'inventory_url') +class AllowListSerializer(ModelSerializer): + """A class providing (de-)serialization of `AllowList` instances.""" + + class Meta: + """Metadata defined for the Django REST Framework.""" + + model = AllowList + fields = ('created_at', 'updated_at', 'type', 'allowed', 'content') + + class InfractionSerializer(ModelSerializer): """A class providing (de-)serialization of `Infraction` instances.""" -- cgit v1.2.3 From fbd00e184e60f21630e25a3bcc36c46f05f44bae Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 14 Jul 2020 13:29:10 -0700 Subject: Merge migrations --- pydis_site/apps/api/migrations/0055_merge_20200714_2027.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0055_merge_20200714_2027.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py new file mode 100644 index 00000000..f2a0e638 --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_allow_blank_message_embeds'), + ('api', '0054_user_invalidate_unknown_role'), + ] + + operations = [ + ] -- cgit v1.2.3 From 9208e0ac7943ce4ce14957f49c1597fb723cbd95 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 14 Jul 2020 13:37:19 -0700 Subject: Allow empty list for user roles This is the same issue as 145beb37fcb4fa2f487f18b234dd72bc4e10c279. See that commit for more information. --- .../api/migrations/0056_allow_blank_user_roles.py | 21 +++++++++++++++++++++ pydis_site/apps/api/models/bot/user.py | 1 + 2 files changed, 22 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py new file mode 100644 index 00000000..489941c7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:35 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_merge_20200714_2027'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/models/bot/user.py b/pydis_site/apps/api/models/bot/user.py index bff4d642..0d8c574a 100644 --- a/pydis_site/apps/api/models/bot/user.py +++ b/pydis_site/apps/api/models/bot/user.py @@ -52,6 +52,7 @@ class User(ModelReprMixin, models.Model): ) ), default=list, + blank=True, help_text="IDs of roles the user has on the server" ) in_guild = models.BooleanField( -- cgit v1.2.3 From 3293eec240a0a2c7bfd8116d75484794d3a6210e Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Tue, 14 Jul 2020 23:04:27 +0200 Subject: Removes django_crispy_bulma dependency. --- Pipfile | 1 - Pipfile.lock | 419 ++++++++++++------------- pydis_site/apps/home/forms/account_deletion.py | 7 +- pydis_site/settings.py | 3 +- 4 files changed, 203 insertions(+), 227 deletions(-) (limited to 'pydis_site') diff --git a/Pipfile b/Pipfile index ef05d090..0f8872ea 100644 --- a/Pipfile +++ b/Pipfile @@ -13,7 +13,6 @@ djangorestframework = "~=3.9.2" djangorestframework-bulk = "~=0.2.1" psycopg2-binary = "~=2.8" django-simple-bulma = ">=1.1.7,<2.0" -django-crispy-bulma = ">=0.1.2,<2.0" whitenoise = "==4.1.2" requests = "~=2.21" pygments = "~=2.3.1" diff --git a/Pipfile.lock b/Pipfile.lock index bc0187bb..3d35f4c3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "59427be022c8717e155b979772436843a4c1e37e2915874a6ee19957e9a0d141" + "sha256": "809dbb7f9b5383ead59aa1706b755eb60149cc9c1160c8516f2d4f54a193a482" }, "pipfile-spec": 6, "requires": { @@ -25,10 +25,10 @@ }, "certifi": { "hashes": [ - "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", - "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" ], - "version": "==2020.4.5.1" + "version": "==2020.6.20" }, "chardet": { "hashes": [ @@ -46,18 +46,18 @@ }, "django": { "hashes": [ - "sha256:84f370f6acedbe1f3c41e1a02de44ac206efda3355e427139ecb785b5f596d80", - "sha256:e8fe3c2b2212dce6126becab7a693157f1a441a07b62ec994c046c76af5bb66d" + "sha256:edf0ecf6657713b0435b6757e6069466925cae70d634a3283c96b80c01e06191", + "sha256:f2250bd35d0f6c23e930c544629934144e5dd39a4c06092e1050c731c1712ba8" ], "index": "pypi", - "version": "==2.2.13" + "version": "==2.2.14" }, "django-allauth": { "hashes": [ - "sha256:7ab91485b80d231da191d5c7999ba93170ef1bf14ab6487d886598a1ad03e1d8" + "sha256:f17209410b7f87da0a84639fd79d3771b596a6d3fc1a8e48ce50dabc7f441d30" ], "index": "pypi", - "version": "==0.41.0" + "version": "==0.42.0" }, "django-classy-tags": { "hashes": [ @@ -65,14 +65,6 @@ ], "version": "==1.0.0" }, - "django-crispy-bulma": { - "hashes": [ - "sha256:6ce8f6df87442040fbba39baede7aba7026d71359376e3ce7eecb7695b09c02b", - "sha256:72a61a1ed87611c87347553f57879dd05e21b4cedffaf9c676971488a0f065b2" - ], - "index": "pypi", - "version": "==0.2" - }, "django-crispy-forms": { "hashes": [ "sha256:5952bab971110d0b86c278132dae0aa095beee8f723e625c3d3fa28888f1675f", @@ -157,17 +149,17 @@ }, "html5lib": { "hashes": [ - "sha256:20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3", - "sha256:66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736" + "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", + "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f" ], - "version": "==1.0.1" + "version": "==1.1" }, "idna": { "hashes": [ - "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", - "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.9" + "version": "==2.10" }, "libsass": { "hashes": [ @@ -203,68 +195,70 @@ }, "pillow": { "hashes": [ - "sha256:04766c4930c174b46fd72d450674612ab44cca977ebbcc2dde722c6933290107", - "sha256:0e2a3bceb0fd4e0cb17192ae506d5f082b309ffe5fc370a5667959c9b2f85fa3", - "sha256:0f01e63c34f0e1e2580cc0b24e86a5ccbbfa8830909a52ee17624c4193224cd9", - "sha256:12e4bad6bddd8546a2f9771485c7e3d2b546b458ae8ff79621214119ac244523", - "sha256:1f694e28c169655c50bb89a3fa07f3b854d71eb47f50783621de813979ba87f3", - "sha256:3d25dd8d688f7318dca6d8cd4f962a360ee40346c15893ae3b95c061cdbc4079", - "sha256:4b02b9c27fad2054932e89f39703646d0c543f21d3cc5b8e05434215121c28cd", - "sha256:9744350687459234867cbebfe9df8f35ef9e1538f3e729adbd8fde0761adb705", - "sha256:a0b49960110bc6ff5fead46013bcb8825d101026d466f3a4de3476defe0fb0dd", - "sha256:ae2b270f9a0b8822b98655cb3a59cdb1bd54a34807c6c56b76dd2e786c3b7db3", - "sha256:b37bb3bd35edf53125b0ff257822afa6962649995cbdfde2791ddb62b239f891", - "sha256:b532bcc2f008e96fd9241177ec580829dee817b090532f43e54074ecffdcd97f", - "sha256:b67a6c47ed963c709ed24566daa3f95a18f07d3831334da570c71da53d97d088", - "sha256:b943e71c2065ade6fef223358e56c167fc6ce31c50bc7a02dd5c17ee4338e8ac", - "sha256:ccc9ad2460eb5bee5642eaf75a0438d7f8887d484490d5117b98edd7f33118b7", - "sha256:d23e2aa9b969cf9c26edfb4b56307792b8b374202810bd949effd1c6e11ebd6d", - "sha256:eaa83729eab9c60884f362ada982d3a06beaa6cc8b084cf9f76cae7739481dfa", - "sha256:ee94fce8d003ac9fd206496f2707efe9eadcb278d94c271f129ab36aa7181344", - "sha256:f455efb7a98557412dc6f8e463c1faf1f1911ec2432059fa3e582b6000fc90e2", - "sha256:f46e0e024346e1474083c729d50de909974237c72daca05393ee32389dabe457", - "sha256:f54be399340aa602066adb63a86a6a5d4f395adfdd9da2b9a0162ea808c7b276", - "sha256:f784aad988f12c80aacfa5b381ec21fd3f38f851720f652b9f33facc5101cf4d" - ], - "version": "==7.1.2" + "sha256:0295442429645fa16d05bd567ef5cff178482439c9aad0411d3f0ce9b88b3a6f", + "sha256:06aba4169e78c439d528fdeb34762c3b61a70813527a2c57f0540541e9f433a8", + "sha256:09d7f9e64289cb40c2c8d7ad674b2ed6105f55dc3b09aa8e4918e20a0311e7ad", + "sha256:0a80dd307a5d8440b0a08bd7b81617e04d870e40a3e46a32d9c246e54705e86f", + "sha256:1ca594126d3c4def54babee699c055a913efb01e106c309fa6b04405d474d5ae", + "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d", + "sha256:431b15cffbf949e89df2f7b48528be18b78bfa5177cb3036284a5508159492b5", + "sha256:52125833b070791fcb5710fabc640fc1df07d087fc0c0f02d3661f76c23c5b8b", + "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8", + "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233", + "sha256:6d7741e65835716ceea0fd13a7d0192961212fd59e741a46bbed7a473c634ed6", + "sha256:6edb5446f44d901e8683ffb25ebdfc26988ee813da3bf91e12252b57ac163727", + "sha256:725aa6cfc66ce2857d585f06e9519a1cc0ef6d13f186ff3447ab6dff0a09bc7f", + "sha256:8dad18b69f710bf3a001d2bf3afab7c432785d94fcf819c16b5207b1cfd17d38", + "sha256:94cf49723928eb6070a892cb39d6c156f7b5a2db4e8971cb958f7b6b104fb4c4", + "sha256:97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626", + "sha256:9ad7f865eebde135d526bb3163d0b23ffff365cf87e767c649550964ad72785d", + "sha256:a060cf8aa332052df2158e5a119303965be92c3da6f2d93b6878f0ebca80b2f6", + "sha256:c79f9c5fb846285f943aafeafda3358992d64f0ef58566e23484132ecd8d7d63", + "sha256:c92302a33138409e8f1ad16731568c55c9053eee71bb05b6b744067e1b62380f", + "sha256:d08b23fdb388c0715990cbc06866db554e1822c4bdcf6d4166cf30ac82df8c41", + "sha256:d350f0f2c2421e65fbc62690f26b59b0bcda1b614beb318c81e38647e0f673a1", + "sha256:ec29604081f10f16a7aea809ad42e27764188fc258b02259a03a8ff7ded3808d", + "sha256:edf31f1150778abd4322444c393ab9c7bd2af271dd4dafb4208fb613b1f3cdc9", + "sha256:f7e30c27477dffc3e85c2463b3e649f751789e0f6c8456099eea7ddd53be4a8a", + "sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce" + ], + "version": "==7.2.0" }, "psycopg2-binary": { "hashes": [ - "sha256:040234f8a4a8dfd692662a8308d78f63f31a97e1c42d2480e5e6810c48966a29", - "sha256:086f7e89ec85a6704db51f68f0dcae432eff9300809723a6e8782c41c2f48e03", - "sha256:18ca813fdb17bc1db73fe61b196b05dd1ca2165b884dd5ec5568877cabf9b039", - "sha256:19dc39616850342a2a6db70559af55b22955f86667b5f652f40c0e99253d9881", - "sha256:2166e770cb98f02ed5ee2b0b569d40db26788e0bf2ec3ae1a0d864ea6f1d8309", - "sha256:3a2522b1d9178575acee4adf8fd9f979f9c0449b00b4164bb63c3475ea6528ed", - "sha256:3aa773580f85a28ffdf6f862e59cb5a3cc7ef6885121f2de3fca8d6ada4dbf3b", - "sha256:3b5deaa3ee7180585a296af33e14c9b18c218d148e735c7accf78130765a47e3", - "sha256:407af6d7e46593415f216c7f56ba087a9a42bd6dc2ecb86028760aa45b802bd7", - "sha256:4c3c09fb674401f630626310bcaf6cd6285daf0d5e4c26d6e55ca26a2734e39b", - "sha256:4c6717962247445b4f9e21c962ea61d2e884fc17df5ddf5e35863b016f8a1f03", - "sha256:50446fae5681fc99f87e505d4e77c9407e683ab60c555ec302f9ac9bffa61103", - "sha256:5057669b6a66aa9ca118a2a860159f0ee3acf837eda937bdd2a64f3431361a2d", - "sha256:5dd90c5438b4f935c9d01fcbad3620253da89d19c1f5fca9158646407ed7df35", - "sha256:659c815b5b8e2a55193ede2795c1e2349b8011497310bb936da7d4745652823b", - "sha256:69b13fdf12878b10dc6003acc8d0abf3ad93e79813fd5f3812497c1c9fb9be49", - "sha256:7a1cb80e35e1ccea3e11a48afe65d38744a0e0bde88795cc56a4d05b6e4f9d70", - "sha256:7e6e3c52e6732c219c07bd97fff6c088f8df4dae3b79752ee3a817e6f32e177e", - "sha256:7f42a8490c4fe854325504ce7a6e4796b207960dabb2cbafe3c3959cb00d1d7e", - "sha256:84156313f258eafff716b2961644a4483a9be44a5d43551d554844d15d4d224e", - "sha256:8578d6b8192e4c805e85f187bc530d0f52ba86c39172e61cd51f68fddd648103", - "sha256:890167d5091279a27e2505ff0e1fb273f8c48c41d35c5b92adbf4af80e6b2ed6", - "sha256:98e10634792ac0e9e7a92a76b4991b44c2325d3e7798270a808407355e7bb0a1", - "sha256:9aadff9032e967865f9778485571e93908d27dab21d0fdfdec0ca779bb6f8ad9", - "sha256:9f24f383a298a0c0f9b3113b982e21751a8ecde6615494a3f1470eb4a9d70e9e", - "sha256:a73021b44813b5c84eda4a3af5826dd72356a900bac9bd9dd1f0f81ee1c22c2f", - "sha256:afd96845e12638d2c44d213d4810a08f4dc4a563f9a98204b7428e567014b1cd", - "sha256:b73ddf033d8cd4cc9dfed6324b1ad2a89ba52c410ef6877998422fcb9c23e3a8", - "sha256:b8f490f5fad1767a1331df1259763b3bad7d7af12a75b950c2843ba319b2415f", - "sha256:dbc5cd56fff1a6152ca59445178652756f4e509f672e49ccdf3d79c1043113a4", - "sha256:eac8a3499754790187bb00574ab980df13e754777d346f85e0ff6df929bcd964", - "sha256:eaed1c65f461a959284649e37b5051224f4db6ebdc84e40b5e65f2986f101a08" - ], - "index": "pypi", - "version": "==2.8.4" + "sha256:008da3ab51adc70a5f1cfbbe5db3a22607ab030eb44bcecf517ad11a0c2b3cac", + "sha256:07cf82c870ec2d2ce94d18e70c13323c89f2f2a2628cbf1feee700630be2519a", + "sha256:08507efbe532029adee21b8d4c999170a83760d38249936038bd0602327029b5", + "sha256:107d9be3b614e52a192719c6bf32e8813030020ea1d1215daa86ded9a24d8b04", + "sha256:17a0ea0b0eabf07035e5e0d520dabc7950aeb15a17c6d36128ba99b2721b25b1", + "sha256:3286541b9d85a340ee4ed42732d15fc1bb441dc500c97243a768154ab8505bb5", + "sha256:3939cf75fc89c5e9ed836e228c4a63604dff95ad19aed2bbf71d5d04c15ed5ce", + "sha256:40abc319f7f26c042a11658bf3dd3b0b3bceccf883ec1c565d5c909a90204434", + "sha256:51f7823f1b087d2020d8e8c9e6687473d3d239ba9afc162d9b2ab6e80b53f9f9", + "sha256:6bb2dd006a46a4a4ce95201f836194eb6a1e863f69ee5bab506673e0ca767057", + "sha256:702f09d8f77dc4794651f650828791af82f7c2efd8c91ae79e3d9fe4bb7d4c98", + "sha256:7036ccf715925251fac969f4da9ad37e4b7e211b1e920860148a10c0de963522", + "sha256:7b832d76cc65c092abd9505cc670c4e3421fd136fb6ea5b94efbe4c146572505", + "sha256:8f74e631b67482d504d7e9cf364071fc5d54c28e79a093ff402d5f8f81e23bfa", + "sha256:930315ac53dc65cbf52ab6b6d27422611f5fb461d763c531db229c7e1af6c0b3", + "sha256:96d3038f5bd061401996614f65d27a4ecb62d843eb4f48e212e6d129171a721f", + "sha256:a20299ee0ea2f9cca494396ac472d6e636745652a64a418b39522c120fd0a0a4", + "sha256:a34826d6465c2e2bbe9d0605f944f19d2480589f89863ed5f091943be27c9de4", + "sha256:a69970ee896e21db4c57e398646af9edc71c003bc52a3cc77fb150240fefd266", + "sha256:b9a8b391c2b0321e0cd7ec6b4cfcc3dd6349347bd1207d48bcb752aa6c553a66", + "sha256:ba13346ff6d3eb2dca0b6fa0d8a9d999eff3dcd9b55f3a890f12b0b6362b2b38", + "sha256:bb0608694a91db1e230b4a314e8ed00ad07ed0c518f9a69b83af2717e31291a3", + "sha256:c8830b7d5f16fd79d39b21e3d94f247219036b29b30c8270314c46bf8b732389", + "sha256:cac918cd7c4c498a60f5d2a61d4f0a6091c2c9490d81bc805c963444032d0dab", + "sha256:cc30cb900f42c8a246e2cb76539d9726f407330bc244ca7729c41a44e8d807fb", + "sha256:ccdc6a87f32b491129ada4b87a43b1895cf2c20fdb7f98ad979647506ffc41b6", + "sha256:d1a8b01f6a964fec702d6b6dac1f91f2b9f9fe41b310cbb16c7ef1fac82df06d", + "sha256:e004db88e5a75e5fdab1620fb9f90c9598c2a195a594225ac4ed2a6f1c23e162", + "sha256:eb2f43ae3037f1ef5e19339c41cf56947021ac892f668765cd65f8ab9814192e", + "sha256:fa466306fcf6b39b8a61d003123d442b23707d635a5cb05ac4e1b62cc79105cd" + ], + "index": "pypi", + "version": "==2.8.5" }, "pygments": { "hashes": [ @@ -276,10 +270,10 @@ }, "python3-openid": { "hashes": [ - "sha256:0086da6b6ef3161cfe50fb1ee5cceaf2cda1700019fda03c2c5c440ca6abe4fa", - "sha256:628d365d687e12da12d02c6691170f4451db28d6d68d050007e4a40065868502" + "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf", + "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b" ], - "version": "==3.1.0" + "version": "==3.2.0" }, "pytz": { "hashes": [ @@ -290,61 +284,47 @@ }, "pyuwsgi": { "hashes": [ - "sha256:15a4626740753b0d0dfeeac7d367f9b2e89ab6af16c195927e60f75359fc1bbc", - "sha256:24c40c3b889eb9f283d43feffbc0f7c7fc024e914451425156ddb68af3df1e71", - "sha256:393737bd43a7e38f0a4a1601a37a69c4bf893635b37665ff958170fdb604fdb7", - "sha256:5a08308f87e639573c1efaa5966a6d04410cd45a73c4586a932fe3ee4b56369d", - "sha256:5f4b36c0dbb9931c4da8008aa423158be596e3b4a23cec95a958631603a94e45", - "sha256:7c31794f71bbd0ccf542cab6bddf38aa69e84e31ae0f9657a2e18ebdc150c01a", - "sha256:802ec6dad4b6707b934370926ec1866603abe31ba03c472f56149001b3533ba1", - "sha256:814d73d4569add69a6c19bb4a27cd5adb72b196e5e080caed17dbda740402072", - "sha256:829299cd117cf8abe837796bf587e61ce6bfe18423a3a1c510c21e9825789c2c", - "sha256:85f2210ceae5f48b7d8fad2240d831f4b890cac85cd98ca82683ac6aa481dfc8", - "sha256:861c94442b28cd64af033e88e0f63c66dbd5609f67952dc18694098b47a43f3a", - "sha256:957bc6316ffc8463795d56d9953d58e7f32aa5aad1c5ac80bc45c69f3299961e", - "sha256:9760c3f56fb5f15852d163429096600906478e9ed2c189a52f2bb21d8a2a986c", - "sha256:9fdfb98a2992de01e8efad2aeed22c825e36db628b144b2d6b93d81fb549f811", - "sha256:a4b24703ea818196d0be1dc64b3b57b79c67e8dee0cfa207a4216220912035a7", - "sha256:ad7f4968c1ddbf139a306d9b075360d959cc554d994ba5e1f512af9a40e62357", - "sha256:b1127d34b90f74faf1707718c57a4193ac028b9f4aec0238638983132297d456", - "sha256:bcb04d6ec644b3e08d03c64851e06edd7110489261e50627a4bcadf66ff6920e", - "sha256:bebfebb9ee83d7cf37668bf54275b677b7ae283e84a944f9f3ac6a4b66f95d4b", - "sha256:c29892dafc65a8b6eb95823fa4bac7754ca3fd1c28ab8d2a973289531b340a27", - "sha256:cb296b50b51ba022b0090b28d032ff1dd395a6db03672b65a39e83532edad527", - "sha256:ce777ebdf49ce736fc04abf555b5c41ab3f130127543a689dcf8d4871cd18fe4", - "sha256:d8b4bf930b6a19bc9ee982b9163d948c87501ad91b71516924e8ed25fe85d2ee", - "sha256:e2a420f2c4d35f3ec0b7e752a80d7bd385e2c5a64f67c05f2d2d74230e3114b6", - "sha256:ef5eb630f541af6b69378d58594be90a0922fa6d6a50a9248c25b9502585f6bf", - "sha256:fed899ce96f4f2b4d1b9f338dd145a4040ee1d8a5152213af0dd8d4a4d36e9fe" + "sha256:1a4dd8d99b8497f109755e09484b0bd2aeaa533f7621e7c7e2a120a72111219d", + "sha256:206937deaebbac5c87692657c3151a5a9d40ecbc9b051b94154205c50a48e963", + "sha256:2cf35d9145208cc7c96464d688caa3de745bfc969e1a1ae23cb046fc10b0ac7e", + "sha256:3ab84a168633eeb55847d59475d86e9078d913d190c2a1aed804c562a10301a3", + "sha256:430406d1bcf288a87f14fde51c66877eaf5e98516838a1c6f761af5d814936fc", + "sha256:72be25ce7aa86c5616c59d12c2961b938e7bde47b7ff6a996ff83b89f7c5cd27", + "sha256:aa4d615de430e2066a1c76d9cc2a70abf2dfc703a82c21aee625b445866f2c3b", + "sha256:aadd231256a672cf4342ef9fb976051949e4d5b616195e696bcb7b8a9c07789e", + "sha256:b15ee6a7759b0465786d856334b8231d882deda5291cf243be6a343a8f3ef910", + "sha256:bd1d0a8d4cb87eb63417a72e6b1bac47053f9b0be550adc6d2a375f4cbaa22f0", + "sha256:d5787779ec24b67ac8898be9dc2b2b4e35f17d79f14361f6cf303d6283a848f2", + "sha256:ecfae85d6504e0ecbba100a795032a88ce8f110b62b93243f2df1bd116eca67f" ], "index": "pypi", "markers": "sys_platform != 'win32'", - "version": "==2.0.18.post0" + "version": "==2.0.19.1" }, "pyyaml": { "hashes": [ - "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", - "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf", - "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5", - "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e", - "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811", - "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e", - "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d", - "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20", - "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689", - "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994", - "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615" + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", + "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", + "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" ], "index": "pypi", - "version": "==5.3" + "version": "==5.3.1" }, "requests": { "hashes": [ - "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", - "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], "index": "pypi", - "version": "==2.23.0" + "version": "==2.24.0" }, "requests-oauthlib": { "hashes": [ @@ -355,11 +335,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:480eee754e60bcae983787a9a13bc8f155a111aef199afaa4f289d6a76aa622a", - "sha256:a920387dc3ee252a66679d0afecd34479fb6fc52c2bc20763793ed69e5b0dcc0" + "sha256:2f023ff348359ec5f0b73a840e8b08e6a8d3b2613a98c57d11c222ef43879237", + "sha256:380a280cfc7c4ade5912294e6d9aa71ce776b5fca60a3782e9331b0bcd2866bf" ], "index": "pypi", - "version": "==0.14.2" + "version": "==0.16.1" }, "six": { "hashes": [ @@ -444,53 +424,50 @@ }, "coverage": { "hashes": [ - "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3", - "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c", - "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0", - "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477", - "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a", - "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf", - "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691", - "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73", - "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987", - "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894", - "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e", - "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef", - "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf", - "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68", - "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8", - "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954", - "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2", - "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40", - "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc", - "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc", - "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e", - "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d", - "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f", - "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc", - "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301", - "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea", - "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb", - "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af", - "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52", - "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37", - "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0" - ], - "index": "pypi", - "version": "==5.0.3" + "sha256:0fc4e0d91350d6f43ef6a61f64a48e917637e1dcfcba4b4b7d543c628ef82c2d", + "sha256:10f2a618a6e75adf64329f828a6a5b40244c1c50f5ef4ce4109e904e69c71bd2", + "sha256:12eaccd86d9a373aea59869bc9cfa0ab6ba8b1477752110cb4c10d165474f703", + "sha256:1874bdc943654ba46d28f179c1846f5710eda3aeb265ff029e0ac2b52daae404", + "sha256:1dcebae667b73fd4aa69237e6afb39abc2f27520f2358590c1b13dd90e32abe7", + "sha256:1e58fca3d9ec1a423f1b7f2aa34af4f733cbfa9020c8fe39ca451b6071237405", + "sha256:214eb2110217f2636a9329bc766507ab71a3a06a8ea30cdeebb47c24dce5972d", + "sha256:25fe74b5b2f1b4abb11e103bb7984daca8f8292683957d0738cd692f6a7cc64c", + "sha256:32ecee61a43be509b91a526819717d5e5650e009a8d5eda8631a59c721d5f3b6", + "sha256:3740b796015b889e46c260ff18b84683fa2e30f0f75a171fb10d2bf9fb91fc70", + "sha256:3b2c34690f613525672697910894b60d15800ac7e779fbd0fccf532486c1ba40", + "sha256:41d88736c42f4a22c494c32cc48a05828236e37c991bd9760f8923415e3169e4", + "sha256:42fa45a29f1059eda4d3c7b509589cc0343cd6bbf083d6118216830cd1a51613", + "sha256:4bb385a747e6ae8a65290b3df60d6c8a692a5599dc66c9fa3520e667886f2e10", + "sha256:509294f3e76d3f26b35083973fbc952e01e1727656d979b11182f273f08aa80b", + "sha256:5c74c5b6045969b07c9fb36b665c9cac84d6c174a809fc1b21bdc06c7836d9a0", + "sha256:60a3d36297b65c7f78329b80120f72947140f45b5c7a017ea730f9112b40f2ec", + "sha256:6f91b4492c5cde83bfe462f5b2b997cdf96a138f7c58b1140f05de5751623cf1", + "sha256:7403675df5e27745571aba1c957c7da2dacb537c21e14007ec3a417bf31f7f3d", + "sha256:87bdc8135b8ee739840eee19b184804e5d57f518578ffc797f5afa2c3c297913", + "sha256:8a3decd12e7934d0254939e2bf434bf04a5890c5bf91a982685021786a08087e", + "sha256:9702e2cb1c6dec01fb8e1a64c015817c0800a6eca287552c47a5ee0ebddccf62", + "sha256:a4d511012beb967a39580ba7d2549edf1e6865a33e5fe51e4dce550522b3ac0e", + "sha256:bbb387811f7a18bdc61a2ea3d102be0c7e239b0db9c83be7bfa50f095db5b92a", + "sha256:bfcc811883699ed49afc58b1ed9f80428a18eb9166422bce3c31a53dba00fd1d", + "sha256:c32aa13cc3fe86b0f744dfe35a7f879ee33ac0a560684fef0f3e1580352b818f", + "sha256:ca63dae130a2e788f2b249200f01d7fa240f24da0596501d387a50e57aa7075e", + "sha256:d54d7ea74cc00482a2410d63bf10aa34ebe1c49ac50779652106c867f9986d6b", + "sha256:d67599521dff98ec8c34cd9652cbcfe16ed076a2209625fca9dc7419b6370e5c", + "sha256:d82db1b9a92cb5c67661ca6616bdca6ff931deceebb98eecbd328812dab52032", + "sha256:d9ad0a988ae20face62520785ec3595a5e64f35a21762a57d115dae0b8fb894a", + "sha256:ebf2431b2d457ae5217f3a1179533c456f3272ded16f8ed0b32961a6d90e38ee", + "sha256:ed9a21502e9223f563e071759f769c3d6a2e1ba5328c31e86830368e8d78bc9c", + "sha256:f50632ef2d749f541ca8e6c07c9928a37f87505ce3a9f20c8446ad310f1aa87b" + ], + "index": "pypi", + "version": "==5.2" }, "distlib": { "hashes": [ - "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21" + "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", + "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1" ], - "version": "==0.3.0" - }, - "entrypoints": { - "hashes": [ - "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", - "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" - ], - "version": "==0.3" + "version": "==0.3.1" }, "filelock": { "hashes": [ @@ -501,19 +478,19 @@ }, "flake8": { "hashes": [ - "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", - "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" + "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", + "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" ], "index": "pypi", - "version": "==3.7.9" + "version": "==3.8.3" }, "flake8-annotations": { "hashes": [ - "sha256:a38b44d01abd480586a92a02a2b0a36231ec42dcc5e114de78fa5db016d8d3f9", - "sha256:d5b0e8704e4e7728b352fa1464e23539ff2341ba11cc153b536fa2cf921ee659" + "sha256:7816a5d8f65ffdf37b8e21e5b17e0fd1e492aa92638573276de066e889a22b26", + "sha256:8d18db74a750dd97f40b483cc3ef80d07d03f687525bad8fd83365dcd3bfd414" ], "index": "pypi", - "version": "==2.0.1" + "version": "==2.3.0" }, "flake8-bandit": { "hashes": [ @@ -563,11 +540,11 @@ }, "flake8-tidy-imports": { "hashes": [ - "sha256:8aa34384b45137d4cf33f5818b8e7897dc903b1d1e10a503fa7dd193a9a710ba", - "sha256:b26461561bcc80e8012e46846630ecf0aaa59314f362a94cb7800dfdb32fa413" + "sha256:62059ca07d8a4926b561d392cbab7f09ee042350214a25cf12823384a45d27dd", + "sha256:c30b40337a2e6802ba3bb611c26611154a27e94c53fc45639e3e282169574fd3" ], "index": "pypi", - "version": "==4.0.0" + "version": "==4.1.0" }, "flake8-todo": { "hashes": [ @@ -585,25 +562,25 @@ }, "gitpython": { "hashes": [ - "sha256:e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a", - "sha256:ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac" + "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858", + "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5" ], - "version": "==3.1.3" + "version": "==3.1.7" }, "identify": { "hashes": [ - "sha256:249ebc7e2066d6393d27c1b1be3b70433f824a120b1d8274d362f1eb419e3b52", - "sha256:781fd3401f5d2b17b22a8b18b493a48d5d948e3330634e82742e23f9c20234ef" + "sha256:882c4b08b4569517b5f2257ecca180e01f38400a17f429f5d0edff55530c41c7", + "sha256:f89add935982d5bc62913ceee16c9297d8ff14b226e9d3072383a4e38136b656" ], - "version": "==1.4.19" + "version": "==1.4.23" }, "importlib-metadata": { "hashes": [ - "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545", - "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958" + "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83", + "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070" ], "markers": "python_version < '3.8'", - "version": "==1.6.1" + "version": "==1.7.0" }, "mccabe": { "hashes": [ @@ -628,26 +605,26 @@ }, "pep8-naming": { "hashes": [ - "sha256:45f330db8fcfb0fba57458c77385e288e7a3be1d01e8ea4268263ef677ceea5f", - "sha256:a33d38177056321a167decd6ba70b890856ba5025f0a8eca6a3eda607da93caf" + "sha256:a1dd47dd243adfe8a83616e27cf03164960b507530f155db94e10b36a6cd6724", + "sha256:f43bfe3eea7e0d73e8b5d07d6407ab47f2476ccaeff6937c84275cd30b016738" ], "index": "pypi", - "version": "==0.9.1" + "version": "==0.11.1" }, "pre-commit": { "hashes": [ - "sha256:09ebe467f43ce24377f8c2f200fe3cd2570d328eb2ce0568c8e96ce19da45fa6", - "sha256:f8d555e31e2051892c7f7b3ad9f620bd2c09271d87e9eedb2ad831737d6211eb" + "sha256:1657663fdd63a321a4a739915d7d03baedd555b25054449090f97bb0cb30a915", + "sha256:e8b1315c585052e729ab7e99dcca5698266bedce9067d21dc909c23e3ceed626" ], "index": "pypi", - "version": "==2.1.1" + "version": "==2.6.0" }, "pycodestyle": { "hashes": [ - "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", - "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", + "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], - "version": "==2.5.0" + "version": "==2.6.0" }, "pydocstyle": { "hashes": [ @@ -658,27 +635,27 @@ }, "pyflakes": { "hashes": [ - "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", - "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", + "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], - "version": "==2.1.1" + "version": "==2.2.0" }, "pyyaml": { "hashes": [ - "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", - "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf", - "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5", - "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e", - "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811", - "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e", - "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d", - "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20", - "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689", - "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994", - "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615" + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", + "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", + "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" ], "index": "pypi", - "version": "==5.3" + "version": "==5.3.1" }, "six": { "hashes": [ @@ -703,10 +680,10 @@ }, "stevedore": { "hashes": [ - "sha256:001e90cd704be6470d46cc9076434e2d0d566c1379187e7013eb296d3a6032d9", - "sha256:471c920412265cc809540ae6fb01f3f02aba89c79bbc7091372f4745a50f9691" + "sha256:79270bd5fb4a052e76932e9fef6e19afa77090c4000f2680eb8c2e887d2e6e36", + "sha256:9fb12884b510fdc25f8a883bb390b8ff82f67863fb360891a33135bcb2ce8c54" ], - "version": "==2.0.0" + "version": "==3.1.0" }, "toml": { "hashes": [ @@ -752,10 +729,10 @@ }, "virtualenv": { "hashes": [ - "sha256:a116629d4e7f4d03433b8afa27f43deba09d48bc48f5ecefa4f015a178efb6cf", - "sha256:a730548b27366c5e6cbdf6f97406d861cccece2e22275e8e1a757aeff5e00c70" + "sha256:c11a475400e98450403c0364eb3a2d25d42f71cf1493da64390487b666de4324", + "sha256:e10cc66f40cbda459720dfe1d334c4dc15add0d80f09108224f171006a97a172" ], - "version": "==20.0.21" + "version": "==20.0.26" }, "zipp": { "hashes": [ diff --git a/pydis_site/apps/home/forms/account_deletion.py b/pydis_site/apps/home/forms/account_deletion.py index 17ffe5c1..9498a341 100644 --- a/pydis_site/apps/home/forms/account_deletion.py +++ b/pydis_site/apps/home/forms/account_deletion.py @@ -1,7 +1,6 @@ from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout +from crispy_forms.layout import Layout, Submit, Field from django.forms import CharField, Form -from django_crispy_bulma.layout import IconField, Submit class AccountDeletionForm(Form): @@ -12,10 +11,10 @@ class AccountDeletionForm(Form): self.helper = FormHelper() self.helper.form_method = "post" - self.helper.add_input(Submit("submit", "I understand, delete my account")) + self.helper.add_input(Submit("submit", "I understand, delete my account", css_class='button is-primary')) self.helper.layout = Layout( - IconField("username", icon_prepend="user") + Field("username") ) username = CharField( diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 5f80a414..4487a9d3 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -52,6 +52,8 @@ if DEBUG: 'api.pythondiscord.local', 'admin.pythondiscord.local', 'staff.pythondiscord.local', + '0.0.0.0', + 'localhost', 'web', 'api.web', 'admin.web', @@ -105,7 +107,6 @@ INSTALLED_APPS = [ 'allauth.socialaccount.providers.github', 'crispy_forms', - 'django_crispy_bulma', 'django_hosts', 'django_filters', 'django_nyt.apps.DjangoNytConfig', -- cgit v1.2.3 From 11503b660d2a4249edde02a50889ebae270f07f4 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Wed, 15 Jul 2020 00:06:25 +0200 Subject: Removes django_crispy_forms dependency, too. --- Pipfile | 1 - Pipfile.lock | 10 +--------- pydis_site/apps/home/forms/account_deletion.py | 14 -------------- pydis_site/settings.py | 13 ------------- pydis_site/templates/home/account/delete.html | 9 ++++++--- 5 files changed, 7 insertions(+), 40 deletions(-) (limited to 'pydis_site') diff --git a/Pipfile b/Pipfile index 0f8872ea..ab77e824 100644 --- a/Pipfile +++ b/Pipfile @@ -5,7 +5,6 @@ verify_ssl = true [packages] django = "~=2.2.13" -django-crispy-forms = "~=1.7.2" django-environ = "~=0.4.5" django-filter = "~=2.1.0" django-hosts = "~=3.0" diff --git a/Pipfile.lock b/Pipfile.lock index 3d35f4c3..3166b224 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "809dbb7f9b5383ead59aa1706b755eb60149cc9c1160c8516f2d4f54a193a482" + "sha256": "009be40262ffc13460a05cefe0e66c41c4c14ed9ea7aca9854ae7bddeeba5da9" }, "pipfile-spec": 6, "requires": { @@ -65,14 +65,6 @@ ], "version": "==1.0.0" }, - "django-crispy-forms": { - "hashes": [ - "sha256:5952bab971110d0b86c278132dae0aa095beee8f723e625c3d3fa28888f1675f", - "sha256:705ededc554ad8736157c666681165fe22ead2dec0d5446d65fc9dd976a5a876" - ], - "index": "pypi", - "version": "==1.7.2" - }, "django-environ": { "hashes": [ "sha256:6c9d87660142608f63ec7d5ce5564c49b603ea8ff25da595fd6098f6dc82afde", diff --git a/pydis_site/apps/home/forms/account_deletion.py b/pydis_site/apps/home/forms/account_deletion.py index 9498a341..b2160657 100644 --- a/pydis_site/apps/home/forms/account_deletion.py +++ b/pydis_site/apps/home/forms/account_deletion.py @@ -1,22 +1,8 @@ -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Submit, Field from django.forms import CharField, Form class AccountDeletionForm(Form): """Account deletion form, to collect username for confirmation of removal.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - - self.helper.form_method = "post" - self.helper.add_input(Submit("submit", "I understand, delete my account", css_class='button is-primary')) - - self.helper.layout = Layout( - Field("username") - ) - username = CharField( label="Username", required=True diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 4487a9d3..206bec7d 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -106,7 +106,6 @@ INSTALLED_APPS = [ 'allauth.socialaccount.providers.discord', 'allauth.socialaccount.providers.github', - 'crispy_forms', 'django_hosts', 'django_filters', 'django_nyt.apps.DjangoNytConfig', @@ -289,7 +288,6 @@ LOGGING = { } # Django Messages framework config - MESSAGE_TAGS = { messages.DEBUG: 'primary', messages.INFO: 'info', @@ -298,17 +296,6 @@ MESSAGE_TAGS = { messages.ERROR: 'danger', } -# Custom settings for Crispyforms -CRISPY_ALLOWED_TEMPLATE_PACKS = ( - "bootstrap", - "uni_form", - "bootstrap3", - "bootstrap4", - "bulma", -) - -CRISPY_TEMPLATE_PACK = "bulma" - # Custom settings for django-simple-bulma BULMA_SETTINGS = { "variables": { # If you update these colours, please update the notification.css file diff --git a/pydis_site/templates/home/account/delete.html b/pydis_site/templates/home/account/delete.html index 1020a82b..0d44e32a 100644 --- a/pydis_site/templates/home/account/delete.html +++ b/pydis_site/templates/home/account/delete.html @@ -1,6 +1,4 @@ {% extends 'base/base.html' %} - -{% load crispy_forms_tags %} {% load static %} {% block title %}Delete Account{% endblock %} @@ -36,7 +34,12 @@
- {% crispy form %} +
+ {% csrf_token %} + + + +
-- cgit v1.2.3 From 4df7239adb60114f833e5c22eacff726b61e1d5a Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Wed, 15 Jul 2020 00:11:51 +0200 Subject: Fix linting issues caused by refactor. --- pydis_site/apps/home/forms/account_deletion.py | 1 + pydis_site/settings.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/home/forms/account_deletion.py b/pydis_site/apps/home/forms/account_deletion.py index b2160657..eec70bea 100644 --- a/pydis_site/apps/home/forms/account_deletion.py +++ b/pydis_site/apps/home/forms/account_deletion.py @@ -3,6 +3,7 @@ from django.forms import CharField, Form class AccountDeletionForm(Form): """Account deletion form, to collect username for confirmation of removal.""" + username = CharField( label="Username", required=True diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 206bec7d..2c87007c 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -52,7 +52,7 @@ if DEBUG: 'api.pythondiscord.local', 'admin.pythondiscord.local', 'staff.pythondiscord.local', - '0.0.0.0', + '0.0.0.0', # noqa: S104 'localhost', 'web', 'api.web', -- cgit v1.2.3 From cb5416bf54e512a66d36b72bff6f7b12a1c8cacd Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Wed, 15 Jul 2020 13:10:44 +0200 Subject: Allowlist viewset. https://github.com/python-discord/site/issues/305 --- pydis_site/apps/api/viewsets/bot/allowlist.py | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 pydis_site/apps/api/viewsets/bot/allowlist.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/bot/allowlist.py b/pydis_site/apps/api/viewsets/bot/allowlist.py new file mode 100644 index 00000000..9b907d05 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/allowlist.py @@ -0,0 +1,54 @@ +from rest_framework.viewsets import ModelViewSet + +from pydis_site.apps.api.models.bot.allowlist import AllowList +from pydis_site.apps.api.serializers import AllowListSerializer + + +class AllowListViewSet(ModelViewSet): + """ + View providing CRUD operations on items whitelisted or blacklisted by our bot. + + ## Routes + ### GET /bot/allowlists + Returns all allowlist items in the database. + + #### Response format + >>> [ + ... { + ... 'id': "2309268224", + ... 'created_at': "01-01-2020 ...", + ... 'updated_at': "01-01-2020 ...", + ... 'type': "file_format", + ... 'allowed': 'true', + ... 'content': ".jpeg", + ... }, + ... ... + ... ] + + #### Status codes + - 200: returned on success + + ### POST /bot/allowedlists + Adds a single allowedlist item to the database. + + #### Request body + >>> { + ... 'type': str, + ... 'allowed': bool, + ... 'content': str, + ... } + + #### Status codes + - 201: returned on success + - 400: if one of the given fields is invalid + + ### DELETE /bot/allowedlists/ + Deletes the tag with the given `title`. + + #### Status codes + - 204: returned on success + - 404: if a tag with the given `title` does not exist + """ + + serializer_class = AllowListSerializer + queryset = AllowList.objects.all() -- cgit v1.2.3 From 0a73c2a019cd973c00cefbcf239a0bc7ca947cfa Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Wed, 15 Jul 2020 14:27:51 +0200 Subject: Minor fixes for imports and __init__ files. https://github.com/python-discord/site/issues/305 --- pydis_site/apps/api/migrations/0007_tag.py | 2 +- pydis_site/apps/api/migrations/0009_snakefact.py | 2 +- pydis_site/apps/api/migrations/0010_snakeidiom.py | 2 +- pydis_site/apps/api/migrations/0012_specialsnake.py | 2 +- pydis_site/apps/api/migrations/0018_messagedeletioncontext.py | 2 +- pydis_site/apps/api/migrations/0019_deletedmessage.py | 2 +- pydis_site/apps/api/migrations/0020_infraction.py | 2 +- pydis_site/apps/api/migrations/0030_reminder.py | 2 +- pydis_site/apps/api/migrations/0031_nomination.py | 2 +- pydis_site/apps/api/migrations/0032_botsetting.py | 2 +- pydis_site/apps/api/migrations/0035_create_table_log_entry.py | 2 +- pydis_site/apps/api/models/__init__.py | 1 - pydis_site/apps/api/viewsets/__init__.py | 1 + pydis_site/apps/api/viewsets/bot/__init__.py | 1 + 14 files changed, 13 insertions(+), 12 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0007_tag.py b/pydis_site/apps/api/migrations/0007_tag.py index c22715f9..b6d146fe 100644 --- a/pydis_site/apps/api/migrations/0007_tag.py +++ b/pydis_site/apps/api/migrations/0007_tag.py @@ -18,6 +18,6 @@ class Migration(migrations.Migration): ('title', models.CharField(help_text='The title of this tag, shown in searches and providing a quick overview over what this embed contains.', max_length=100, primary_key=True, serialize=False)), ('embed', django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual embed shown by this tag.')), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0009_snakefact.py b/pydis_site/apps/api/migrations/0009_snakefact.py index 4fc63bc9..fd583846 100644 --- a/pydis_site/apps/api/migrations/0009_snakefact.py +++ b/pydis_site/apps/api/migrations/0009_snakefact.py @@ -16,6 +16,6 @@ class Migration(migrations.Migration): fields=[ ('fact', models.CharField(help_text='A fact about snakes.', max_length=200, primary_key=True, serialize=False)), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0010_snakeidiom.py b/pydis_site/apps/api/migrations/0010_snakeidiom.py index be089cf4..7d06ce5f 100644 --- a/pydis_site/apps/api/migrations/0010_snakeidiom.py +++ b/pydis_site/apps/api/migrations/0010_snakeidiom.py @@ -16,6 +16,6 @@ class Migration(migrations.Migration): fields=[ ('idiom', models.CharField(help_text='A snake idiom', max_length=140, primary_key=True, serialize=False)), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0012_specialsnake.py b/pydis_site/apps/api/migrations/0012_specialsnake.py index 77072526..ed0c1563 100644 --- a/pydis_site/apps/api/migrations/0012_specialsnake.py +++ b/pydis_site/apps/api/migrations/0012_specialsnake.py @@ -17,6 +17,6 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=140, primary_key=True, serialize=False)), ('info', models.TextField()), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0018_messagedeletioncontext.py b/pydis_site/apps/api/migrations/0018_messagedeletioncontext.py index dced1288..7e372d04 100644 --- a/pydis_site/apps/api/migrations/0018_messagedeletioncontext.py +++ b/pydis_site/apps/api/migrations/0018_messagedeletioncontext.py @@ -19,6 +19,6 @@ class Migration(migrations.Migration): ('creation', models.DateTimeField(help_text='When this deletion took place.')), ('actor', models.ForeignKey(help_text='The original actor causing this deletion. Could be the author of a manual clean command invocation, the bot when executing automatic actions, or nothing to indicate that the bulk deletion was not issued by us.', null=True, on_delete=django.db.models.deletion.CASCADE, to='api.User')), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0019_deletedmessage.py b/pydis_site/apps/api/migrations/0019_deletedmessage.py index 4b028f0c..33746253 100644 --- a/pydis_site/apps/api/migrations/0019_deletedmessage.py +++ b/pydis_site/apps/api/migrations/0019_deletedmessage.py @@ -25,6 +25,6 @@ class Migration(migrations.Migration): options={ 'abstract': False, }, - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0020_infraction.py b/pydis_site/apps/api/migrations/0020_infraction.py index 6bef6b77..96c71687 100644 --- a/pydis_site/apps/api/migrations/0020_infraction.py +++ b/pydis_site/apps/api/migrations/0020_infraction.py @@ -25,6 +25,6 @@ class Migration(migrations.Migration): ('actor', models.ForeignKey(help_text='The user which applied the infraction.', on_delete=django.db.models.deletion.CASCADE, related_name='infractions_given', to='api.User')), ('user', models.ForeignKey(help_text='The user to which the infraction was applied.', on_delete=django.db.models.deletion.CASCADE, related_name='infractions_received', to='api.User')), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0030_reminder.py b/pydis_site/apps/api/migrations/0030_reminder.py index 8c42f6dc..e1f1afc3 100644 --- a/pydis_site/apps/api/migrations/0030_reminder.py +++ b/pydis_site/apps/api/migrations/0030_reminder.py @@ -22,6 +22,6 @@ class Migration(migrations.Migration): ('expiration', models.DateTimeField(help_text='When this reminder should be sent.')), ('author', models.ForeignKey(help_text='The creator of this reminder.', on_delete=django.db.models.deletion.CASCADE, to='api.User')), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0031_nomination.py b/pydis_site/apps/api/migrations/0031_nomination.py index 75e69701..f39436c1 100644 --- a/pydis_site/apps/api/migrations/0031_nomination.py +++ b/pydis_site/apps/api/migrations/0031_nomination.py @@ -21,6 +21,6 @@ class Migration(migrations.Migration): ('inserted_at', models.DateTimeField(auto_now_add=True, help_text='The creation date of this nomination.')), ('author', models.ForeignKey(help_text='The staff member that nominated this user.', on_delete=django.db.models.deletion.CASCADE, related_name='nomination_set', to='api.User')), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0032_botsetting.py b/pydis_site/apps/api/migrations/0032_botsetting.py index 25186a2b..3304edef 100644 --- a/pydis_site/apps/api/migrations/0032_botsetting.py +++ b/pydis_site/apps/api/migrations/0032_botsetting.py @@ -18,6 +18,6 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=50, primary_key=True, serialize=False)), ('data', django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual settings of this setting.')), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/migrations/0035_create_table_log_entry.py b/pydis_site/apps/api/migrations/0035_create_table_log_entry.py index a8256a0e..c9a1ad19 100644 --- a/pydis_site/apps/api/migrations/0035_create_table_log_entry.py +++ b/pydis_site/apps/api/migrations/0035_create_table_log_entry.py @@ -24,6 +24,6 @@ class Migration(migrations.Migration): ('line', models.PositiveSmallIntegerField(help_text='The line at which the log line was emitted.')), ('message', models.TextField(help_text='The textual content of the log line.')), ], - bases=(pydis_site.apps.api.models.ModelReprMixin, models.Model), + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), ] diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index 04d0fc50..2839fbba 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -16,4 +16,3 @@ from .bot import ( User ) from .log_entry import LogEntry -from .mixins import ModelReprMixin, ModelTimestampMixin diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py index 3cf9f641..98d3d586 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa from .bot import ( + AllowListViewSet, BotSettingViewSet, DeletedMessageViewSet, DocumentationLinkViewSet, diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index b3e0fa4d..86bfc910 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -1,4 +1,5 @@ # flake8: noqa +from .allowlist import AllowListViewSet from .bot_setting import BotSettingViewSet from .deleted_message import DeletedMessageViewSet from .documentation_link import DocumentationLinkViewSet -- cgit v1.2.3 From d03ac5fbf06bc3749e68a606601c0b793f1f0766 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Wed, 15 Jul 2020 14:29:32 +0200 Subject: Set up url forwarding for the viewset. https://github.com/python-discord/site/issues/305 --- Pipfile.lock | 31 +---------------------------- pydis_site/apps/api/models/bot/allowlist.py | 16 +++++++-------- pydis_site/apps/api/urls.py | 21 ++++++++++++++----- 3 files changed, 24 insertions(+), 44 deletions(-) (limited to 'pydis_site') diff --git a/Pipfile.lock b/Pipfile.lock index 097c4f81..9cd105f5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -21,7 +21,6 @@ "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" ], - "markers": "python_version >= '3.5'", "version": "==3.2.10" }, "bleach": { @@ -29,7 +28,6 @@ "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f", "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.1.5" }, "certifi": { @@ -51,7 +49,6 @@ "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93", "sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.6.0" }, "django": { @@ -111,7 +108,6 @@ "sha256:90eb236eb4f1a92124bd7c37852bbe09c0d21158477cc237556d59842a91c509", "sha256:dfdb3af75ad27cdd4458b0544ec8574174f2b90f99bc2cafab6a15b4bc1895a8" ], - "markers": "python_version >= '3.5'", "version": "==0.11.0" }, "django-nyt": { @@ -155,7 +151,6 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "importlib-metadata": { @@ -189,7 +184,6 @@ "sha256:1fafe3f1ecabfb514a5285fca634a53c1b32a81cb0feb154264d55bf2ff22c17", "sha256:c467cd6233885534bf0fe96e62e3cf46cfc1605112356c4f9981512b8174de59" ], - "markers": "python_version >= '3.5'", "version": "==3.2.2" }, "oauthlib": { @@ -197,7 +191,6 @@ "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.1.0" }, "packaging": { @@ -205,7 +198,6 @@ "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.4" }, "pillow": { @@ -237,7 +229,6 @@ "sha256:f7e30c27477dffc3e85c2463b3e649f751789e0f6c8456099eea7ddd53be4a8a", "sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce" ], - "markers": "python_version >= '3.5'", "version": "==7.2.0" }, "psycopg2-binary": { @@ -289,7 +280,6 @@ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "python3-openid": { @@ -353,8 +343,7 @@ "requests-oauthlib": { "hashes": [ "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", - "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", - "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" + "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a" ], "version": "==1.3.0" }, @@ -371,7 +360,6 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "sorl-thumbnail": { @@ -379,7 +367,6 @@ "sha256:66771521f3c0ed771e1ce8e1aaf1639ebff18f7f5a40cfd3083da8f0fe6c7c99", "sha256:7162639057dff222a651bacbdb6bd6f558fc32946531d541fc71e10c0167ebdf" ], - "markers": "python_version >= '3.4'", "version": "==12.6.3" }, "sqlparse": { @@ -387,7 +374,6 @@ "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.3.1" }, "urllib3": { @@ -395,7 +381,6 @@ "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.25.9" }, "webencodings": { @@ -426,7 +411,6 @@ "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" ], - "markers": "python_version >= '3.6'", "version": "==3.1.0" } }, @@ -443,7 +427,6 @@ "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==19.3.0" }, "bandit": { @@ -458,7 +441,6 @@ "sha256:1ccf53320421aeeb915275a196e23b3b8ae87dea8ac6698b1638001d4a486d53", "sha256:c8e8f552ffcc6194f4e18dd4f68d9aef0c0d58ae7e7be8c82bee3c5e9edfa513" ], - "markers": "python_full_version >= '3.6.1'", "version": "==3.1.0" }, "coverage": { @@ -597,7 +579,6 @@ "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac", "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9" ], - "markers": "python_version >= '3.4'", "version": "==4.0.5" }, "gitpython": { @@ -605,7 +586,6 @@ "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858", "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5" ], - "markers": "python_version >= '3.4'", "version": "==3.1.7" }, "identify": { @@ -613,7 +593,6 @@ "sha256:882c4b08b4569517b5f2257ecca180e01f38400a17f429f5d0edff55530c41c7", "sha256:f89add935982d5bc62913ceee16c9297d8ff14b226e9d3072383a4e38136b656" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.23" }, "importlib-metadata": { @@ -666,7 +645,6 @@ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pydocstyle": { @@ -674,7 +652,6 @@ "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586", "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5" ], - "markers": "python_version >= '3.5'", "version": "==5.0.2" }, "pyflakes": { @@ -682,7 +659,6 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pyyaml": { @@ -707,7 +683,6 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "smmap": { @@ -715,7 +690,6 @@ "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4", "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==3.0.4" }, "snowballstemmer": { @@ -730,7 +704,6 @@ "sha256:79270bd5fb4a052e76932e9fef6e19afa77090c4000f2680eb8c2e887d2e6e36", "sha256:9fb12884b510fdc25f8a883bb390b8ff82f67863fb360891a33135bcb2ce8c54" ], - "markers": "python_version >= '3.6'", "version": "==3.1.0" }, "toml": { @@ -780,7 +753,6 @@ "sha256:c11a475400e98450403c0364eb3a2d25d42f71cf1493da64390487b666de4324", "sha256:e10cc66f40cbda459720dfe1d334c4dc15add0d80f09108224f171006a97a172" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.0.26" }, "zipp": { @@ -788,7 +760,6 @@ "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" ], - "markers": "python_version >= '3.6'", "version": "==3.1.0" } } diff --git a/pydis_site/apps/api/models/bot/allowlist.py b/pydis_site/apps/api/models/bot/allowlist.py index c8fa2e33..b0aea066 100644 --- a/pydis_site/apps/api/models/bot/allowlist.py +++ b/pydis_site/apps/api/models/bot/allowlist.py @@ -1,23 +1,21 @@ from django.db import models -from pydis_site.apps.api.models import ModelReprMixin, ModelTimestampMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin class AllowList(ModelTimestampMixin, ModelReprMixin, models.Model): """An item that is either allowed or denied.""" AllowListType = models.TextChoices( - 'guild_invite_id', - 'file_format', - 'domain_name', - 'word_watchlist', + 'AllowListType', + 'GUILD_INVITE_ID ' + 'FILE_FORMAT ' + 'DOMAIN_NAME ' + 'WORD_WATCHLIST ' ) type = models.CharField( max_length=50, - help_text=( - "The type of allowlist this is on. The value must be one of the following: " - f"{','.join(AllowListType.choices)}." - ), + help_text="The type of allowlist this is on.", choices=AllowListType.choices, ) allowed = models.BooleanField( diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index 3bb5198e..bf41f09f 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -3,17 +3,28 @@ from rest_framework.routers import DefaultRouter from .views import HealthcheckView, RulesView from .viewsets import ( - BotSettingViewSet, DeletedMessageViewSet, - DocumentationLinkViewSet, InfractionViewSet, - LogEntryViewSet, NominationViewSet, + AllowListViewSet, + BotSettingViewSet, + DeletedMessageViewSet, + DocumentationLinkViewSet, + InfractionViewSet, + LogEntryViewSet, + NominationViewSet, OffTopicChannelNameViewSet, - OffensiveMessageViewSet, ReminderViewSet, - RoleViewSet, TagViewSet, UserViewSet + OffensiveMessageViewSet, + ReminderViewSet, + RoleViewSet, + TagViewSet, + UserViewSet ) # http://www.django-rest-framework.org/api-guide/routers/#defaultrouter bot_router = DefaultRouter(trailing_slash=False) +bot_router.register( + 'allowlists', + AllowListViewSet +) bot_router.register( 'bot-settings', BotSettingViewSet -- cgit v1.2.3 From 787b317b4617df7931c96697126b26ea7e2a217d Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Wed, 15 Jul 2020 14:29:54 +0200 Subject: Add a migration for the new AllowList model. https://github.com/python-discord/site/issues/305 --- pydis_site/apps/api/migrations/0057_allowlist.py | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0057_allowlist.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0057_allowlist.py b/pydis_site/apps/api/migrations/0057_allowlist.py new file mode 100644 index 00000000..7d815e91 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_allowlist.py @@ -0,0 +1,29 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0056_allow_blank_user_roles'), + ] + + operations = [ + migrations.CreateModel( + name='AllowList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField(choices=[('GUILD_INVITE_ID', 'Guild Invite Id'), ('FILE_FORMAT', 'File Format'), ('DOMAIN_NAME', 'Domain Name'), ('WORD_WATCHLIST', 'Word Watchlist')], help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allowlist.')), + ], + options={ + 'abstract': False, + }, + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + ] -- cgit v1.2.3 From e8a32c717babee132626d6574f7ca706338739dc Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Wed, 15 Jul 2020 15:32:23 +0200 Subject: Add a UniqueConstraint to prevent duplicates. https://github.com/python-discord/site/issues/305 --- pydis_site/apps/api/migrations/0057_allowlist.py | 29 ----------------- .../migrations/0057_create_new_allowlist_model.py | 37 ++++++++++++++++++++++ pydis_site/apps/api/models/bot/allowlist.py | 13 ++++++++ 3 files changed, 50 insertions(+), 29 deletions(-) delete mode 100644 pydis_site/apps/api/migrations/0057_allowlist.py create mode 100644 pydis_site/apps/api/migrations/0057_create_new_allowlist_model.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0057_allowlist.py b/pydis_site/apps/api/migrations/0057_allowlist.py deleted file mode 100644 index 7d815e91..00000000 --- a/pydis_site/apps/api/migrations/0057_allowlist.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0056_allow_blank_user_roles'), - ] - - operations = [ - migrations.CreateModel( - name='AllowList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField(choices=[('GUILD_INVITE_ID', 'Guild Invite Id'), ('FILE_FORMAT', 'File Format'), ('DOMAIN_NAME', 'Domain Name'), ('WORD_WATCHLIST', 'Word Watchlist')], help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allowlist.')), - ], - options={ - 'abstract': False, - }, - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - ] diff --git a/pydis_site/apps/api/migrations/0057_create_new_allowlist_model.py b/pydis_site/apps/api/migrations/0057_create_new_allowlist_model.py new file mode 100644 index 00000000..45650d86 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_create_new_allowlist_model.py @@ -0,0 +1,37 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0056_allow_blank_user_roles'), + ] + + operations = [ + migrations.CreateModel( + name='AllowList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField(choices=[('GUILD_INVITE_ID', 'Guild Invite Id'), ('FILE_FORMAT', 'File Format'), ('DOMAIN_NAME', 'Domain Name'), ('WORD_WATCHLIST', 'Word Watchlist')], help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allowlist.')), + ], + options={ + 'abstract': False, + }, + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AlterModelTable( + name='allowlist', + table='allow_list', + ), + migrations.AddConstraint( + model_name='allowlist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_allowlist'), + ) + ] diff --git a/pydis_site/apps/api/models/bot/allowlist.py b/pydis_site/apps/api/models/bot/allowlist.py index b0aea066..fc57ef32 100644 --- a/pydis_site/apps/api/models/bot/allowlist.py +++ b/pydis_site/apps/api/models/bot/allowlist.py @@ -24,3 +24,16 @@ class AllowList(ModelTimestampMixin, ModelReprMixin, models.Model): content = models.TextField( help_text="The data to add to the allowlist." ) + + class Meta: + """Metaconfig for this model.""" + + db_table = 'allow_list' + + # This constraint ensures only one allowlist with the same content + # can exist per type.This means that we cannot have both an allow + # and a deny for the same item, and we cannot have duplicates of the + # same item. + constraints = [ + models.UniqueConstraint(fields=['content', 'type'], name='unique_allowlist'), + ] -- cgit v1.2.3 From 90f325fe94af4a771ea4ed0ddfc5a074d0e88c07 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Wed, 15 Jul 2020 15:34:30 +0200 Subject: Return id from the AllowListSerializer. https://github.com/python-discord/site/issues/305 --- pydis_site/apps/api/serializers.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 24ba0ec0..13030074 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -4,13 +4,20 @@ from rest_framework.validators import UniqueTogetherValidator from rest_framework_bulk import BulkSerializerMixin from .models import ( - BotSetting, DeletedMessage, - DocumentationLink, Infraction, - LogEntry, MessageDeletionContext, - Nomination, OffTopicChannelName, - OffensiveMessage, AllowList, - Reminder, Role, - Tag, User + AllowList, + BotSetting, + DeletedMessage, + DocumentationLink, + Infraction, + LogEntry, + MessageDeletionContext, + Nomination, + OffTopicChannelName, + OffensiveMessage, + Reminder, + Role, + Tag, + User, ) @@ -104,7 +111,7 @@ class AllowListSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = AllowList - fields = ('created_at', 'updated_at', 'type', 'allowed', 'content') + fields = ('id', 'created_at', 'updated_at', 'type', 'allowed', 'content') class InfractionSerializer(ModelSerializer): -- cgit v1.2.3 From 9fdfdc2a0867aac7fe271412ae41c9349df77180 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Wed, 15 Jul 2020 14:19:41 -0700 Subject: Bump jQuery version The Django wiki updated the jQuery version, so our reference needs to be updated too. Otherwise, we will get a 404 for the script. --- pydis_site/templates/wiki/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/templates/wiki/base.html b/pydis_site/templates/wiki/base.html index 9f904324..846492ab 100644 --- a/pydis_site/templates/wiki/base.html +++ b/pydis_site/templates/wiki/base.html @@ -7,7 +7,7 @@ {% block head %} {{ block.super }} - + -- cgit v1.2.3 From 2c88104434ad1af68877959059ef04a69eae5c33 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Thu, 16 Jul 2020 10:38:14 +0800 Subject: Add mentions field to Reminder model --- pydis_site/apps/api/models/bot/reminder.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/bot/reminder.py b/pydis_site/apps/api/models/bot/reminder.py index d53fedb5..4b5d15ca 100644 --- a/pydis_site/apps/api/models/bot/reminder.py +++ b/pydis_site/apps/api/models/bot/reminder.py @@ -1,3 +1,4 @@ +from django.contrib.postgres.fields import ArrayField from django.core.validators import MinValueValidator from django.db import models @@ -45,6 +46,19 @@ class Reminder(ModelReprMixin, models.Model): expiration = models.DateTimeField( help_text="When this reminder should be sent." ) + mentions = ArrayField( + models.BigIntegerField( + validators=( + MinValueValidator( + limit_value=0, + message="Mention IDs cannot be negative." + ), + ) + ), + default=list, + blank=True, + help_text="IDs of roles or users to ping with the reminder." + ) def __str__(self): """Returns some info on the current reminder, for display purposes.""" -- cgit v1.2.3 From cdbda598fafccdd4ef0a324e7b18ce070aaf2d70 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Thu, 16 Jul 2020 10:38:58 +0800 Subject: Return mentions from ReminderSerializer --- pydis_site/apps/api/serializers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index f2d5144c..80e552a6 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -203,7 +203,9 @@ class ReminderSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = Reminder - fields = ('active', 'author', 'jump_url', 'channel_id', 'content', 'expiration', 'id') + fields = ( + 'active', 'author', 'jump_url', 'channel_id', 'content', 'expiration', 'id', 'mentions' + ) class RoleSerializer(ModelSerializer): -- cgit v1.2.3 From 1e72079691fb1acf61390edad496736d332362ca Mon Sep 17 00:00:00 2001 From: kosayoda Date: Thu, 16 Jul 2020 10:39:23 +0800 Subject: Document mentions in ReminderViewSet --- pydis_site/apps/api/viewsets/bot/reminder.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py index 147f6dbc..5a44b7d3 100644 --- a/pydis_site/apps/api/viewsets/bot/reminder.py +++ b/pydis_site/apps/api/viewsets/bot/reminder.py @@ -27,6 +27,11 @@ class ReminderViewSet( ... { ... 'active': True, ... 'author': 1020103901030, + ... 'mentions': [ + ... 336843820513755157, + ... 165023948638126080, + ... 267628507062992896 + ... ], ... 'content': "Make dinner", ... 'expiration': '5018-11-20T15:52:00Z', ... 'id': 11 -- cgit v1.2.3 From 02412121e8272d35a03c30a80a4a67e6aabdf0eb Mon Sep 17 00:00:00 2001 From: kosayoda Date: Thu, 16 Jul 2020 10:40:03 +0800 Subject: Add migration for the mentions field in the Reminder model --- .../apps/api/migrations/0055_reminder_mentions.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0055_reminder_mentions.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py new file mode 100644 index 00000000..d73b450d --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_reminder_mentions.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.14 on 2020-07-15 07:37 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0054_user_invalidate_unknown_role'), + ] + + operations = [ + migrations.AddField( + model_name='reminder', + name='mentions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), + ), + ] -- cgit v1.2.3 From 4c288387097dcb65feadce360857c9fbe1b2593f Mon Sep 17 00:00:00 2001 From: kosayoda Date: Thu, 16 Jul 2020 10:46:56 +0800 Subject: Document POSTing mentions in ReminderViewSet --- pydis_site/apps/api/viewsets/bot/reminder.py | 1 + 1 file changed, 1 insertion(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py index 5a44b7d3..b31330a0 100644 --- a/pydis_site/apps/api/viewsets/bot/reminder.py +++ b/pydis_site/apps/api/viewsets/bot/reminder.py @@ -48,6 +48,7 @@ class ReminderViewSet( #### Request body >>> { ... 'author': int, + ... 'mentions': List[int], ... 'content': str, ... 'expiration': str # ISO-formatted datetime ... } -- cgit v1.2.3 From 5cc5f5e2c7d4a6fa5b74aa33a6dbc7ffcf4bcc99 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Thu, 16 Jul 2020 10:52:21 +0800 Subject: Document PATCH for reminders --- pydis_site/apps/api/viewsets/bot/reminder.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py index b31330a0..940d19d4 100644 --- a/pydis_site/apps/api/viewsets/bot/reminder.py +++ b/pydis_site/apps/api/viewsets/bot/reminder.py @@ -58,6 +58,22 @@ class ReminderViewSet( - 400: if the body format is invalid - 404: if no user with the given ID could be found + ### PATCH /bot/reminders/ + Update the user with the given `id`. + All fields in the request body are optional. + + #### Request body + >>> { + ... 'mentions': List[int], + ... 'content': str, + ... 'expiration': str # ISO-formatted datetime + ... } + + #### Status codes + - 200: returned on success + - 400: if the body format is invalid + - 404: if no user with the given ID could be found + ### DELETE /bot/reminders/ Delete the reminder with the given `id`. -- cgit v1.2.3 From 806b27bb30ceb32603bd90d31e32ae30bf8f499e Mon Sep 17 00:00:00 2001 From: kosayoda Date: Thu, 16 Jul 2020 14:11:35 +0800 Subject: Document more undocumented stuff --- pydis_site/apps/api/viewsets/bot/reminder.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py index 940d19d4..f4921d44 100644 --- a/pydis_site/apps/api/viewsets/bot/reminder.py +++ b/pydis_site/apps/api/viewsets/bot/reminder.py @@ -34,7 +34,9 @@ class ReminderViewSet( ... ], ... 'content': "Make dinner", ... 'expiration': '5018-11-20T15:52:00Z', - ... 'id': 11 + ... 'id': 11, + ... 'channel_id': 634547009956872193, + ... 'jump_url': "https://discord.com/channels///" ... }, ... ... ... ] @@ -50,7 +52,9 @@ class ReminderViewSet( ... 'author': int, ... 'mentions': List[int], ... 'content': str, - ... 'expiration': str # ISO-formatted datetime + ... 'expiration': str, # ISO-formatted datetime + ... 'channel_id': int, + ... 'jump_url': Optional[str] ... } #### Status codes -- cgit v1.2.3 From 18f055f9f9922033a4ef4172c8fa1eaa9cb2f4cd Mon Sep 17 00:00:00 2001 From: kosayoda Date: Thu, 16 Jul 2020 15:08:28 +0800 Subject: Add mentions field to valid data test --- pydis_site/apps/api/tests/test_reminders.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_reminders.py b/pydis_site/apps/api/tests/test_reminders.py index c7fa07c9..5042ea90 100644 --- a/pydis_site/apps/api/tests/test_reminders.py +++ b/pydis_site/apps/api/tests/test_reminders.py @@ -4,7 +4,7 @@ from django.forms.models import model_to_dict from django_hosts.resolvers import reverse from .base import APISubdomainTestCase -from ..models import Reminder, User +from ..models import Reminder, Role, User class UnauthedReminderAPITests(APISubdomainTestCase): @@ -54,6 +54,18 @@ class ReminderCreationTests(APISubdomainTestCase): name='Mermaid Man', discriminator=1234, ) + cls.user = User.objects.create( + id=5678, + name='Fish Dude', + discriminator=5678, + ) + cls.role = Role.objects.create( + id=555, + name="Random role", + colour=2, + permissions=0b01010010101, + position=10, + ) def test_accepts_valid_data(self): data = { @@ -62,6 +74,7 @@ class ReminderCreationTests(APISubdomainTestCase): 'expiration': datetime.utcnow().isoformat(), 'jump_url': "https://www.google.com", 'channel_id': 123, + 'mentions': [self.user.id, self.role.id], } url = reverse('bot:reminder-list', host='api') response = self.client.post(url, data=data) -- cgit v1.2.3 From fbcf39f2bb05b9746ac911d25c2118ac9cf9dff3 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Thu, 16 Jul 2020 15:52:02 +0800 Subject: Merge migrations --- pydis_site/apps/api/migrations/0057_merge_20200716_0751.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0057_merge_20200716_0751.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py new file mode 100644 index 00000000..47a6d2d4 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.14 on 2020-07-16 07:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_reminder_mentions'), + ('api', '0056_allow_blank_user_roles'), + ] + + operations = [ + ] -- cgit v1.2.3 From 4470afc5b48fa1c71ed717372acad5414e6ec0ad Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Thu, 16 Jul 2020 12:59:43 +0200 Subject: Fix a bug in an old migration. https://github.com/python-discord/site/issues/305 --- .../api/migrations/0025_allow_custom_inserted_at_infraction_field.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py b/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py index 0c02cb91..9589346d 100644 --- a/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py +++ b/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py @@ -1,7 +1,7 @@ # Generated by Django 2.1.4 on 2019-01-06 16:01 -import datetime from django.db import migrations, models +from django.utils import timezone class Migration(migrations.Migration): @@ -14,6 +14,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='infraction', name='inserted_at', - field=models.DateTimeField(default=datetime.datetime.utcnow, help_text='The date and time of the creation of this infraction.'), + field=models.DateTimeField(default=timezone.now(), help_text='The date and time of the creation of this infraction.'), ), ] -- cgit v1.2.3 From e3c750a3fdd1beee441c4d62b0c5fe8644255ea6 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Thu, 16 Jul 2020 13:00:21 +0200 Subject: Add tests for the AllowList model. https://github.com/python-discord/site/issues/305 --- pydis_site/apps/api/tests/test_allowlists.py | 104 +++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 pydis_site/apps/api/tests/test_allowlists.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_allowlists.py b/pydis_site/apps/api/tests/test_allowlists.py new file mode 100644 index 00000000..c6004439 --- /dev/null +++ b/pydis_site/apps/api/tests/test_allowlists.py @@ -0,0 +1,104 @@ +from django_hosts.resolvers import reverse + +from .base import APISubdomainTestCase +from ..models import AllowList + +URL = reverse('bot:allowlist-list', host='api') +JPEG_ALLOWLIST = { + "type": 'FILE_FORMAT', + "allowed": True, + "content": ".jpeg", +} +PNG_ALLOWLIST = { + "type": 'FILE_FORMAT', + "allowed": True, + "content": ".png", +} + + +class UnauthenticatedTests(APISubdomainTestCase): + def setUp(self): + super().setUp() + self.client.force_authenticate(user=None) + + def test_cannot_read_allowedlist_list(self): + response = self.client.get(URL) + + self.assertEqual(response.status_code, 401) + + +class EmptyDatabaseTests(APISubdomainTestCase): + def test_returns_empty_object(self): + response = self.client.get(URL) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), []) + + +class FetchTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + cls.jpeg_format = AllowList.objects.create(**JPEG_ALLOWLIST) + cls.png_format = AllowList.objects.create(**PNG_ALLOWLIST) + + def test_returns_name_in_list(self): + response = self.client.get(URL) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()[0]["content"], self.jpeg_format.content) + self.assertEqual(response.json()[1]["content"], self.png_format.content) + + def test_returns_single_item_by_id(self): + response = self.client.get(f'{URL}/{self.jpeg_format.id}') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json().get("content"), self.jpeg_format.content) + + +class CreationTests(APISubdomainTestCase): + def test_returns_400_for_missing_params(self): + no_type_json = { + "allowed": True, + "content": ".jpeg" + } + no_allowed_json = { + "type": "FILE_FORMAT", + "content": ".jpeg" + } + no_content_json = { + "allowed": True, + "type": "FILE_FORMAT" + } + cases = [{}, no_type_json, no_allowed_json, no_content_json] + + for case in cases: + response = self.client.post(URL, data=case) + self.assertEqual(response.status_code, 400) + + def test_returns_201_for_successful_creation(self): + response = self.client.post(URL, data=JPEG_ALLOWLIST) + self.assertEqual(response.status_code, 201) + + +class DeletionTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + cls.jpeg_format = AllowList.objects.create(**JPEG_ALLOWLIST) + cls.png_format = AllowList.objects.create(**PNG_ALLOWLIST) + + def test_deleting_unknown_id_returns_404(self): + response = self.client.delete(f"{URL}/200") + + self.assertEqual(response.status_code, 404) + + def test_deleting_known_id_returns_204(self): + response = self.client.delete(f"{URL}/{self.jpeg_format.id}") + + self.assertEqual(response.status_code, 204) + + def test_name_gets_deleted(self): + response = self.client.delete(f"{URL}/{self.png_format.id}") + self.assertEqual(response.status_code, 204) + + response = self.client.get(f"{URL}/{self.png_format.id}") + self.assertNotIn(self.png_format.content, response.json()) -- cgit v1.2.3 From ce3d207a65a888e30e55448d7c902475a03906d3 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Thu, 16 Jul 2020 13:27:08 +0200 Subject: Improve some docstrings. https://github.com/python-discord/site/issues/305 --- pydis_site/apps/api/models/mixins.py | 2 ++ pydis_site/apps/api/viewsets/bot/allowlist.py | 30 +++++++++++++++++++++------ 2 files changed, 26 insertions(+), 6 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py index 942edaa1..5d75b78b 100644 --- a/pydis_site/apps/api/models/mixins.py +++ b/pydis_site/apps/api/models/mixins.py @@ -26,4 +26,6 @@ class ModelTimestampMixin(models.Model): updated_at = models.DateTimeField(auto_now=True) class Meta: + """Metaconfig for the mixin.""" + abstract = True diff --git a/pydis_site/apps/api/viewsets/bot/allowlist.py b/pydis_site/apps/api/viewsets/bot/allowlist.py index 9b907d05..7cc82ff7 100644 --- a/pydis_site/apps/api/viewsets/bot/allowlist.py +++ b/pydis_site/apps/api/viewsets/bot/allowlist.py @@ -27,27 +27,45 @@ class AllowListViewSet(ModelViewSet): #### Status codes - 200: returned on success + - 401: returned if unauthenticated + + ### GET /bot/allowlists/ + Returns a specific AllowList item from the database. + + #### Response format + >>> { + ... 'id': "2309268224", + ... 'created_at': "01-01-2020 ...", + ... 'updated_at': "01-01-2020 ...", + ... 'type': "file_format", + ... 'allowed': 'true', + ... 'content': ".jpeg", + ... } + + #### Status codes + - 200: returned on success + - 404: returned if the id was not found. ### POST /bot/allowedlists Adds a single allowedlist item to the database. #### Request body >>> { - ... 'type': str, - ... 'allowed': bool, - ... 'content': str, + ... 'type': str, + ... 'allowed': bool, + ... 'content': str, ... } #### Status codes - 201: returned on success - 400: if one of the given fields is invalid - ### DELETE /bot/allowedlists/ - Deletes the tag with the given `title`. + ### DELETE /bot/allowedlists/ + Deletes the tag with the given `id`. #### Status codes - 204: returned on success - - 404: if a tag with the given `title` does not exist + - 404: if a tag with the given `id` does not exist """ serializer_class = AllowListSerializer -- cgit v1.2.3 From e2aabf684433af3b08079182179fcd964ead8a71 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Thu, 16 Jul 2020 20:23:26 +0200 Subject: Fix some broken tests. The test_utils_account.py tests were never running, because the folder they were in had no __init__.py file. The test_models.py file was failing because it had an outdated import of the ModelReprMixin, which has moved to a new file. https://github.com/python-discord/site/issues/305 --- pydis_site/apps/api/tests/test_models.py | 4 ++-- pydis_site/tests/__init__.py | 0 pydis_site/tests/test_utils_account.py | 11 +++++------ 3 files changed, 7 insertions(+), 8 deletions(-) create mode 100644 pydis_site/tests/__init__.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index b4754484..e0e347bb 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -3,13 +3,12 @@ from datetime import datetime as dt from django.test import SimpleTestCase from django.utils import timezone -from ..models import ( +from pydis_site.apps.api.models import ( DeletedMessage, DocumentationLink, Infraction, Message, MessageDeletionContext, - ModelReprMixin, Nomination, OffTopicChannelName, OffensiveMessage, @@ -18,6 +17,7 @@ from ..models import ( Tag, User ) +from pydis_site.apps.api.models.mixins import ModelReprMixin class SimpleClass(ModelReprMixin): diff --git a/pydis_site/tests/__init__.py b/pydis_site/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pydis_site/tests/test_utils_account.py b/pydis_site/tests/test_utils_account.py index e8db7b64..3edca658 100644 --- a/pydis_site/tests/test_utils_account.py +++ b/pydis_site/tests/test_utils_account.py @@ -5,7 +5,7 @@ from allauth.socialaccount.models import SocialAccount, SocialLogin from django.contrib.auth.models import User from django.contrib.messages.storage.base import BaseStorage from django.http import HttpRequest -from django.test import TestCase +from django.test import RequestFactory, TestCase from pydis_site.apps.api.models import Role, User as DiscordUser from pydis_site.utils.account import AccountAdapter, SocialAccountAdapter @@ -74,6 +74,8 @@ class AccountUtilsTests(TestCase): self.discord_user_two_roles.roles.append(developers_role.id) + self.request_factory = RequestFactory() + def test_account_adapter(self): """Test that our Allauth account adapter functions correctly.""" adapter = AccountAdapter() @@ -86,12 +88,11 @@ class AccountUtilsTests(TestCase): discord_login = SocialLogin(account=self.discord_account) discord_login_role = SocialLogin(account=self.discord_account_role) - discord_login_two_roles = SocialLogin(account=self.discord_account_two_roles) discord_login_not_present = SocialLogin(account=self.discord_account_not_present) github_login = SocialLogin(account=self.github_account) - messages_request = HttpRequest() + messages_request = self.request_factory.get("/") messages_request._messages = BaseStorage(messages_request) with patch("pydis_site.utils.account.reverse") as mock_reverse: @@ -112,8 +113,6 @@ class AccountUtilsTests(TestCase): self.assertEqual(mock_redirect.call_count, 4) self.assertEqual(mock_reverse.call_count, 4) - self.assertTrue(adapter.is_open_for_signup(HttpRequest(), discord_login_two_roles)) - def test_social_account_adapter_populate(self): """Test that our Allauth social account adapter correctly handles data population.""" adapter = SocialAccountAdapter() @@ -126,7 +125,7 @@ class AccountUtilsTests(TestCase): discord_login.account.extra_data["discriminator"] = "0000" user = adapter.populate_user( - HttpRequest(), discord_login, + self.request_factory.get("/"), discord_login, {"username": "user"} ) -- cgit v1.2.3 From 76cd687715e49cee97bac24f5d3c8ca40ffca099 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Thu, 16 Jul 2020 20:48:56 +0200 Subject: Rename AllowList to AllowDenyList. https://github.com/python-discord/site/issues/305 --- .../0057_create_new_allowdenylist_model.py | 32 ++++++++++ .../migrations/0057_create_new_allowlist_model.py | 37 ----------- pydis_site/apps/api/models/__init__.py | 2 +- pydis_site/apps/api/models/bot/__init__.py | 2 +- pydis_site/apps/api/models/bot/allow_deny_list.py | 37 +++++++++++ pydis_site/apps/api/models/bot/allowlist.py | 39 ------------ pydis_site/apps/api/serializers.py | 8 +-- pydis_site/apps/api/tests/test_allowlists.py | 14 ++--- pydis_site/apps/api/urls.py | 6 +- pydis_site/apps/api/viewsets/__init__.py | 2 +- pydis_site/apps/api/viewsets/bot/__init__.py | 2 +- .../apps/api/viewsets/bot/allow_deny_list.py | 72 ++++++++++++++++++++++ pydis_site/apps/api/viewsets/bot/allowlist.py | 72 ---------------------- 13 files changed, 159 insertions(+), 166 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py delete mode 100644 pydis_site/apps/api/migrations/0057_create_new_allowlist_model.py create mode 100644 pydis_site/apps/api/models/bot/allow_deny_list.py delete mode 100644 pydis_site/apps/api/models/bot/allowlist.py create mode 100644 pydis_site/apps/api/viewsets/bot/allow_deny_list.py delete mode 100644 pydis_site/apps/api/viewsets/bot/allowlist.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py b/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py new file mode 100644 index 00000000..c450344b --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py @@ -0,0 +1,32 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0056_allow_blank_user_roles'), + ] + + operations = [ + migrations.CreateModel( + name='AllowDenyList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField( + choices=[('GUILD_INVITE_ID', 'Guild Invite Id'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('WORD_WATCHLIST', 'Word Watchlist')], + help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='allowdenylist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_allow_deny_list'), + ) + ] diff --git a/pydis_site/apps/api/migrations/0057_create_new_allowlist_model.py b/pydis_site/apps/api/migrations/0057_create_new_allowlist_model.py deleted file mode 100644 index 45650d86..00000000 --- a/pydis_site/apps/api/migrations/0057_create_new_allowlist_model.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0056_allow_blank_user_roles'), - ] - - operations = [ - migrations.CreateModel( - name='AllowList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField(choices=[('GUILD_INVITE_ID', 'Guild Invite Id'), ('FILE_FORMAT', 'File Format'), ('DOMAIN_NAME', 'Domain Name'), ('WORD_WATCHLIST', 'Word Watchlist')], help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allowlist.')), - ], - options={ - 'abstract': False, - }, - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.AlterModelTable( - name='allowlist', - table='allow_list', - ), - migrations.AddConstraint( - model_name='allowlist', - constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_allowlist'), - ) - ] diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index 2839fbba..34973a8d 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa from .bot import ( - AllowList, + AllowDenyList, BotSetting, DocumentationLink, DeletedMessage, diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index b373ee84..1234b35a 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -1,5 +1,5 @@ # flake8: noqa -from .allowlist import AllowList +from .allow_deny_list import AllowDenyList from .bot_setting import BotSetting from .deleted_message import DeletedMessage from .documentation_link import DocumentationLink diff --git a/pydis_site/apps/api/models/bot/allow_deny_list.py b/pydis_site/apps/api/models/bot/allow_deny_list.py new file mode 100644 index 00000000..1eef47ba --- /dev/null +++ b/pydis_site/apps/api/models/bot/allow_deny_list.py @@ -0,0 +1,37 @@ +from django.db import models + +from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin + + +class AllowDenyList(ModelTimestampMixin, ModelReprMixin, models.Model): + """An item that is either allowed or denied.""" + + AllowDenyListType = models.TextChoices( + 'AllowDenyListType', + 'GUILD_INVITE_ID ' + 'FILE_FORMAT ' + 'DOMAIN_NAME ' + 'WORD_WATCHLIST ' + ) + type = models.CharField( + max_length=50, + help_text="The type of allowlist this is on.", + choices=AllowDenyListType.choices, + ) + allowed = models.BooleanField( + help_text="Whether this item is on the allowlist or the denylist." + ) + content = models.TextField( + help_text="The data to add to the allow or denylist." + ) + + class Meta: + """Metaconfig for this model.""" + + # This constraint ensures only one allow or denylist with the + # same content can exist. This means that we cannot have both an allow + # and a deny for the same item, and we cannot have duplicates of the + # same item. + constraints = [ + models.UniqueConstraint(fields=['content', 'type'], name='unique_allow_deny_list'), + ] diff --git a/pydis_site/apps/api/models/bot/allowlist.py b/pydis_site/apps/api/models/bot/allowlist.py deleted file mode 100644 index fc57ef32..00000000 --- a/pydis_site/apps/api/models/bot/allowlist.py +++ /dev/null @@ -1,39 +0,0 @@ -from django.db import models - -from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin - - -class AllowList(ModelTimestampMixin, ModelReprMixin, models.Model): - """An item that is either allowed or denied.""" - - AllowListType = models.TextChoices( - 'AllowListType', - 'GUILD_INVITE_ID ' - 'FILE_FORMAT ' - 'DOMAIN_NAME ' - 'WORD_WATCHLIST ' - ) - type = models.CharField( - max_length=50, - help_text="The type of allowlist this is on.", - choices=AllowListType.choices, - ) - allowed = models.BooleanField( - help_text="Whether this item is on the allowlist or the denylist." - ) - content = models.TextField( - help_text="The data to add to the allowlist." - ) - - class Meta: - """Metaconfig for this model.""" - - db_table = 'allow_list' - - # This constraint ensures only one allowlist with the same content - # can exist per type.This means that we cannot have both an allow - # and a deny for the same item, and we cannot have duplicates of the - # same item. - constraints = [ - models.UniqueConstraint(fields=['content', 'type'], name='unique_allowlist'), - ] diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 13030074..d532dd69 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -4,7 +4,7 @@ from rest_framework.validators import UniqueTogetherValidator from rest_framework_bulk import BulkSerializerMixin from .models import ( - AllowList, + AllowDenyList, BotSetting, DeletedMessage, DocumentationLink, @@ -104,13 +104,13 @@ class DocumentationLinkSerializer(ModelSerializer): fields = ('package', 'base_url', 'inventory_url') -class AllowListSerializer(ModelSerializer): - """A class providing (de-)serialization of `AllowList` instances.""" +class AllowDenyListSerializer(ModelSerializer): + """A class providing (de-)serialization of `AllowDenyList` instances.""" class Meta: """Metadata defined for the Django REST Framework.""" - model = AllowList + model = AllowDenyList fields = ('id', 'created_at', 'updated_at', 'type', 'allowed', 'content') diff --git a/pydis_site/apps/api/tests/test_allowlists.py b/pydis_site/apps/api/tests/test_allowlists.py index c6004439..5aa50670 100644 --- a/pydis_site/apps/api/tests/test_allowlists.py +++ b/pydis_site/apps/api/tests/test_allowlists.py @@ -1,9 +1,9 @@ from django_hosts.resolvers import reverse -from .base import APISubdomainTestCase -from ..models import AllowList +from pydis_site.apps.api.models import AllowDenyList +from pydis_site.apps.api.tests.base import APISubdomainTestCase -URL = reverse('bot:allowlist-list', host='api') +URL = reverse('bot:allowdenylist-list', host='api') JPEG_ALLOWLIST = { "type": 'FILE_FORMAT', "allowed": True, @@ -38,8 +38,8 @@ class EmptyDatabaseTests(APISubdomainTestCase): class FetchTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): - cls.jpeg_format = AllowList.objects.create(**JPEG_ALLOWLIST) - cls.png_format = AllowList.objects.create(**PNG_ALLOWLIST) + cls.jpeg_format = AllowDenyList.objects.create(**JPEG_ALLOWLIST) + cls.png_format = AllowDenyList.objects.create(**PNG_ALLOWLIST) def test_returns_name_in_list(self): response = self.client.get(URL) @@ -83,8 +83,8 @@ class CreationTests(APISubdomainTestCase): class DeletionTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): - cls.jpeg_format = AllowList.objects.create(**JPEG_ALLOWLIST) - cls.png_format = AllowList.objects.create(**PNG_ALLOWLIST) + cls.jpeg_format = AllowDenyList.objects.create(**JPEG_ALLOWLIST) + cls.png_format = AllowDenyList.objects.create(**PNG_ALLOWLIST) def test_deleting_unknown_id_returns_404(self): response = self.client.delete(f"{URL}/200") diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index bf41f09f..b6ed2914 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -3,7 +3,7 @@ from rest_framework.routers import DefaultRouter from .views import HealthcheckView, RulesView from .viewsets import ( - AllowListViewSet, + AllowDenyListViewSet, BotSettingViewSet, DeletedMessageViewSet, DocumentationLinkViewSet, @@ -22,8 +22,8 @@ from .viewsets import ( # http://www.django-rest-framework.org/api-guide/routers/#defaultrouter bot_router = DefaultRouter(trailing_slash=False) bot_router.register( - 'allowlists', - AllowListViewSet + 'allow_deny_lists', + AllowDenyListViewSet ) bot_router.register( 'bot-settings', diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py index 98d3d586..eb7d5098 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa from .bot import ( - AllowListViewSet, + AllowDenyListViewSet, BotSettingViewSet, DeletedMessageViewSet, DocumentationLinkViewSet, diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index 86bfc910..11638dd8 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -1,5 +1,5 @@ # flake8: noqa -from .allowlist import AllowListViewSet +from .allow_deny_list import AllowDenyListViewSet from .bot_setting import BotSettingViewSet from .deleted_message import DeletedMessageViewSet from .documentation_link import DocumentationLinkViewSet diff --git a/pydis_site/apps/api/viewsets/bot/allow_deny_list.py b/pydis_site/apps/api/viewsets/bot/allow_deny_list.py new file mode 100644 index 00000000..a2499d89 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/allow_deny_list.py @@ -0,0 +1,72 @@ +from rest_framework.viewsets import ModelViewSet + +from pydis_site.apps.api.models.bot.allow_deny_list import AllowDenyList +from pydis_site.apps.api.serializers import AllowDenyListSerializer + + +class AllowDenyListViewSet(ModelViewSet): + """ + View providing CRUD operations on items allowed or denied by our bot. + + ## Routes + ### GET /bot/allow_deny_lists + Returns all allow and denylist items in the database. + + #### Response format + >>> [ + ... { + ... 'id': "2309268224", + ... 'created_at': "01-01-2020 ...", + ... 'updated_at': "01-01-2020 ...", + ... 'type': "file_format", + ... 'allowed': 'true', + ... 'content': ".jpeg", + ... }, + ... ... + ... ] + + #### Status codes + - 200: returned on success + - 401: returned if unauthenticated + + ### GET /bot/allow_deny_lists/ + Returns a specific AllowDenyList item from the database. + + #### Response format + >>> { + ... 'id': "2309268224", + ... 'created_at': "01-01-2020 ...", + ... 'updated_at': "01-01-2020 ...", + ... 'type': "file_format", + ... 'allowed': 'true', + ... 'content': ".jpeg", + ... } + + #### Status codes + - 200: returned on success + - 404: returned if the id was not found. + + ### POST /bot/allow_deny_lists + Adds a single AllowDenyList item to the database. + + #### Request body + >>> { + ... 'type': str, + ... 'allowed': bool, + ... 'content': str, + ... } + + #### Status codes + - 201: returned on success + - 400: if one of the given fields is invalid + + ### DELETE /bot/allow_deny_lists/ + Deletes the AllowDenyList item with the given `id`. + + #### Status codes + - 204: returned on success + - 404: if a tag with the given `id` does not exist + """ + + serializer_class = AllowDenyListSerializer + queryset = AllowDenyList.objects.all() diff --git a/pydis_site/apps/api/viewsets/bot/allowlist.py b/pydis_site/apps/api/viewsets/bot/allowlist.py deleted file mode 100644 index 7cc82ff7..00000000 --- a/pydis_site/apps/api/viewsets/bot/allowlist.py +++ /dev/null @@ -1,72 +0,0 @@ -from rest_framework.viewsets import ModelViewSet - -from pydis_site.apps.api.models.bot.allowlist import AllowList -from pydis_site.apps.api.serializers import AllowListSerializer - - -class AllowListViewSet(ModelViewSet): - """ - View providing CRUD operations on items whitelisted or blacklisted by our bot. - - ## Routes - ### GET /bot/allowlists - Returns all allowlist items in the database. - - #### Response format - >>> [ - ... { - ... 'id': "2309268224", - ... 'created_at': "01-01-2020 ...", - ... 'updated_at': "01-01-2020 ...", - ... 'type': "file_format", - ... 'allowed': 'true', - ... 'content': ".jpeg", - ... }, - ... ... - ... ] - - #### Status codes - - 200: returned on success - - 401: returned if unauthenticated - - ### GET /bot/allowlists/ - Returns a specific AllowList item from the database. - - #### Response format - >>> { - ... 'id': "2309268224", - ... 'created_at': "01-01-2020 ...", - ... 'updated_at': "01-01-2020 ...", - ... 'type': "file_format", - ... 'allowed': 'true', - ... 'content': ".jpeg", - ... } - - #### Status codes - - 200: returned on success - - 404: returned if the id was not found. - - ### POST /bot/allowedlists - Adds a single allowedlist item to the database. - - #### Request body - >>> { - ... 'type': str, - ... 'allowed': bool, - ... 'content': str, - ... } - - #### Status codes - - 201: returned on success - - 400: if one of the given fields is invalid - - ### DELETE /bot/allowedlists/ - Deletes the tag with the given `id`. - - #### Status codes - - 204: returned on success - - 404: if a tag with the given `id` does not exist - """ - - serializer_class = AllowListSerializer - queryset = AllowList.objects.all() -- cgit v1.2.3 From f596c4a45b0eb98b0c7bb1b13e79427f5f57a59b Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Thu, 16 Jul 2020 22:25:00 +0200 Subject: 100% coverage for account.py https://github.com/python-discord/site/issues/305 --- pydis_site/tests/test_utils_account.py | 59 +++++++++++++++++----------------- 1 file changed, 30 insertions(+), 29 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/tests/test_utils_account.py b/pydis_site/tests/test_utils_account.py index 3edca658..e12805bb 100644 --- a/pydis_site/tests/test_utils_account.py +++ b/pydis_site/tests/test_utils_account.py @@ -13,28 +13,43 @@ from pydis_site.utils.account import AccountAdapter, SocialAccountAdapter class AccountUtilsTests(TestCase): def setUp(self): + # Create the user self.django_user = User.objects.create(username="user") + # Create the roles + developers_role = Role.objects.create( + id=1, + name="Developers", + colour=0, + permissions=0, + position=1 + ) + everyone_role = Role.objects.create( + id=0, + name="@everyone", + colour=0, + permissions=0, + position=0 + ) + + # Create the social accounts self.discord_account = SocialAccount.objects.create( user=self.django_user, provider="discord", uid=0 ) - - self.discord_account_role = SocialAccount.objects.create( + self.discord_account_one_role = SocialAccount.objects.create( user=self.django_user, provider="discord", uid=1 ) - self.discord_account_two_roles = SocialAccount.objects.create( user=self.django_user, provider="discord", uid=2 ) - self.discord_account_not_present = SocialAccount.objects.create( user=self.django_user, provider="discord", uid=3 ) - self.github_account = SocialAccount.objects.create( user=self.django_user, provider="github", uid=0 ) + # Create DiscordUsers self.discord_user = DiscordUser.objects.create( id=0, name="user", @@ -44,36 +59,17 @@ class AccountUtilsTests(TestCase): self.discord_user_role = DiscordUser.objects.create( id=1, name="user present", - discriminator=0 + discriminator=0, + roles=[everyone_role.id] ) self.discord_user_two_roles = DiscordUser.objects.create( id=2, name="user with both roles", - discriminator=0 - ) - - everyone_role = Role.objects.create( - id=0, - name="@everyone", - colour=0, - permissions=0, - position=0 - ) - - self.discord_user_role.roles.append(everyone_role.id) - self.discord_user_two_roles.roles.append(everyone_role.id) - - developers_role = Role.objects.create( - id=1, - name="Developers", - colour=0, - permissions=0, - position=1 + discriminator=0, + roles=[everyone_role.id, developers_role.id] ) - self.discord_user_two_roles.roles.append(developers_role.id) - self.request_factory = RequestFactory() def test_account_adapter(self): @@ -87,8 +83,9 @@ class AccountUtilsTests(TestCase): adapter = SocialAccountAdapter() discord_login = SocialLogin(account=self.discord_account) - discord_login_role = SocialLogin(account=self.discord_account_role) + discord_login_role = SocialLogin(account=self.discord_account_one_role) discord_login_not_present = SocialLogin(account=self.discord_account_not_present) + discord_login_two_roles = SocialLogin(account=self.discord_account_two_roles) github_login = SocialLogin(account=self.github_account) @@ -109,6 +106,10 @@ class AccountUtilsTests(TestCase): with self.assertRaises(ImmediateHttpResponse): adapter.is_open_for_signup(messages_request, discord_login_not_present) + self.assertTrue( + adapter.is_open_for_signup(messages_request, discord_login_two_roles) + ) + self.assertEqual(len(messages_request._messages._queued_messages), 4) self.assertEqual(mock_redirect.call_count, 4) self.assertEqual(mock_reverse.call_count, 4) -- cgit v1.2.3 From 1fc9034dd10f8d6f02a43af9170005529803e029 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Thu, 16 Jul 2020 22:44:22 +0200 Subject: 100% branch coverage for account.py https://github.com/python-discord/site/issues/305 --- pydis_site/tests/test_utils_account.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/tests/test_utils_account.py b/pydis_site/tests/test_utils_account.py index e12805bb..6f8338b4 100644 --- a/pydis_site/tests/test_utils_account.py +++ b/pydis_site/tests/test_utils_account.py @@ -122,13 +122,18 @@ class AccountUtilsTests(TestCase): account=self.discord_account, user=self.django_user ) - discord_login.account.extra_data["discriminator"] = "0000" - user = adapter.populate_user( + discord_user = adapter.populate_user( self.request_factory.get("/"), discord_login, {"username": "user"} ) + self.assertEqual(discord_user.username, "user#0000") + self.assertEqual(discord_user.first_name, "user#0000") - self.assertEqual(user.username, "user#0000") - self.assertEqual(user.first_name, "user#0000") + discord_login.account.provider = "not_discord" + not_discord_user = adapter.populate_user( + self.request_factory.get("/"), discord_login, + {"username": "user"} + ) + self.assertEqual(not_discord_user.username, "user") -- cgit v1.2.3 From 061ace4c1013abbdb5408f682c0c8401d13a59ec Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Fri, 17 Jul 2020 14:59:59 +0200 Subject: Add an endpoint for getting AllowDenyList types. https://github.com/python-discord/site/issues/305 --- pydis_site/apps/api/tests/test_allowlists.py | 8 ++++++++ pydis_site/apps/api/viewsets/bot/allow_deny_list.py | 8 ++++++++ 2 files changed, 16 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_allowlists.py b/pydis_site/apps/api/tests/test_allowlists.py index 5aa50670..1440dc71 100644 --- a/pydis_site/apps/api/tests/test_allowlists.py +++ b/pydis_site/apps/api/tests/test_allowlists.py @@ -54,6 +54,14 @@ class FetchTests(APISubdomainTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.json().get("content"), self.jpeg_format.content) + def test_returns_allow_deny_list_types(self): + response = self.client.get(f'{URL}/get_types') + + self.assertEqual(response.status_code, 200) + for api_type, model_type in zip(response.json(), AllowDenyList.AllowDenyListType.choices): + self.assertEquals(api_type[0], model_type[0]) + self.assertEquals(api_type[1], model_type[1]) + class CreationTests(APISubdomainTestCase): def test_returns_400_for_missing_params(self): diff --git a/pydis_site/apps/api/viewsets/bot/allow_deny_list.py b/pydis_site/apps/api/viewsets/bot/allow_deny_list.py index a2499d89..2b9fd6a7 100644 --- a/pydis_site/apps/api/viewsets/bot/allow_deny_list.py +++ b/pydis_site/apps/api/viewsets/bot/allow_deny_list.py @@ -1,3 +1,6 @@ +from rest_framework.decorators import action +from rest_framework.request import Request +from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from pydis_site.apps.api.models.bot.allow_deny_list import AllowDenyList @@ -70,3 +73,8 @@ class AllowDenyListViewSet(ModelViewSet): serializer_class = AllowDenyListSerializer queryset = AllowDenyList.objects.all() + + @action(detail=False, methods=["get"]) + def get_types(self, _: Request) -> Response: + """Get a list of all the types of AllowDenyLists we support.""" + return Response(AllowDenyList.AllowDenyListType.choices) -- cgit v1.2.3 From f6111a35e0b5385ccd02cea12d0afc4a86615ba0 Mon Sep 17 00:00:00 2001 From: kosayoda Date: Sun, 19 Jul 2020 12:49:04 +0800 Subject: Use literal integers for mentions ID in test Since the mentions field stores static IDs and not foreign keys, there is no need to create the objects for the test. --- pydis_site/apps/api/tests/test_reminders.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_reminders.py b/pydis_site/apps/api/tests/test_reminders.py index 5042ea90..a05d9296 100644 --- a/pydis_site/apps/api/tests/test_reminders.py +++ b/pydis_site/apps/api/tests/test_reminders.py @@ -4,7 +4,7 @@ from django.forms.models import model_to_dict from django_hosts.resolvers import reverse from .base import APISubdomainTestCase -from ..models import Reminder, Role, User +from ..models import Reminder, User class UnauthedReminderAPITests(APISubdomainTestCase): @@ -54,18 +54,6 @@ class ReminderCreationTests(APISubdomainTestCase): name='Mermaid Man', discriminator=1234, ) - cls.user = User.objects.create( - id=5678, - name='Fish Dude', - discriminator=5678, - ) - cls.role = Role.objects.create( - id=555, - name="Random role", - colour=2, - permissions=0b01010010101, - position=10, - ) def test_accepts_valid_data(self): data = { @@ -74,7 +62,7 @@ class ReminderCreationTests(APISubdomainTestCase): 'expiration': datetime.utcnow().isoformat(), 'jump_url': "https://www.google.com", 'channel_id': 123, - 'mentions': [self.user.id, self.role.id], + 'mentions': [8888, 9999], } url = reverse('bot:reminder-list', host='api') response = self.client.post(url, data=data) -- cgit v1.2.3 From 387b763af7dfe3675a476ab6a2b9815e2ac8e83a Mon Sep 17 00:00:00 2001 From: kosayoda Date: Sun, 19 Jul 2020 12:50:01 +0800 Subject: Fix misleading documentation --- pydis_site/apps/api/viewsets/bot/reminder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py index f4921d44..6f8a28f2 100644 --- a/pydis_site/apps/api/viewsets/bot/reminder.py +++ b/pydis_site/apps/api/viewsets/bot/reminder.py @@ -54,7 +54,7 @@ class ReminderViewSet( ... 'content': str, ... 'expiration': str, # ISO-formatted datetime ... 'channel_id': int, - ... 'jump_url': Optional[str] + ... 'jump_url': str ... } #### Status codes -- cgit v1.2.3 From 8874a83fa6372e0ce46e9552e8c21c182e4a1c3e Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 19 Jul 2020 11:55:20 +0200 Subject: Add another AllowDenyList field, 'comment'. This will be used to describe or justify the entries in the blacklist or whitelist, for example for the guild name in the case of guild invite IDs, so that we have some context when we're displaying the list. https://github.com/python-discord/site/issues/305 --- pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py | 1 + pydis_site/apps/api/models/bot/allow_deny_list.py | 4 ++++ pydis_site/apps/api/serializers.py | 2 +- pydis_site/apps/api/viewsets/bot/allow_deny_list.py | 3 +++ 4 files changed, 9 insertions(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py b/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py index c450344b..d36acd70 100644 --- a/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py +++ b/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py @@ -22,6 +22,7 @@ class Migration(migrations.Migration): help_text='The type of allowlist this is on.', max_length=50)), ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), ], bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), ), diff --git a/pydis_site/apps/api/models/bot/allow_deny_list.py b/pydis_site/apps/api/models/bot/allow_deny_list.py index 1eef47ba..b95dd72e 100644 --- a/pydis_site/apps/api/models/bot/allow_deny_list.py +++ b/pydis_site/apps/api/models/bot/allow_deny_list.py @@ -24,6 +24,10 @@ class AllowDenyList(ModelTimestampMixin, ModelReprMixin, models.Model): content = models.TextField( help_text="The data to add to the allow or denylist." ) + comment = models.TextField( + help_text="Optional comment on this entry.", + null=True + ) class Meta: """Metaconfig for this model.""" diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index d532dd69..50f27f1d 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -111,7 +111,7 @@ class AllowDenyListSerializer(ModelSerializer): """Metadata defined for the Django REST Framework.""" model = AllowDenyList - fields = ('id', 'created_at', 'updated_at', 'type', 'allowed', 'content') + fields = ('id', 'created_at', 'updated_at', 'type', 'allowed', 'content', 'comment') class InfractionSerializer(ModelSerializer): diff --git a/pydis_site/apps/api/viewsets/bot/allow_deny_list.py b/pydis_site/apps/api/viewsets/bot/allow_deny_list.py index 2b9fd6a7..72cbc84c 100644 --- a/pydis_site/apps/api/viewsets/bot/allow_deny_list.py +++ b/pydis_site/apps/api/viewsets/bot/allow_deny_list.py @@ -24,6 +24,7 @@ class AllowDenyListViewSet(ModelViewSet): ... 'type': "file_format", ... 'allowed': 'true', ... 'content': ".jpeg", + ... 'comment': "Popular image format.", ... }, ... ... ... ] @@ -43,6 +44,7 @@ class AllowDenyListViewSet(ModelViewSet): ... 'type': "file_format", ... 'allowed': 'true', ... 'content': ".jpeg", + ... 'comment': "Popular image format.", ... } #### Status codes @@ -57,6 +59,7 @@ class AllowDenyListViewSet(ModelViewSet): ... 'type': str, ... 'allowed': bool, ... 'content': str, + ... 'comment': Optional[str], ... } #### Status codes -- cgit v1.2.3 From 74ef6fee279c451dac165d3792242ffdf35441ff Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 19 Jul 2020 12:45:14 +0200 Subject: Simplify AllowDenyListType names. GUILD_INVITE_ID -> GUILD_INVITE WORD_WATCHLIST -> FILTER_TOKEN --- pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py | 4 ++-- pydis_site/apps/api/models/bot/allow_deny_list.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py b/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py index d36acd70..ab8c5f3f 100644 --- a/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py +++ b/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py @@ -17,8 +17,8 @@ class Migration(migrations.Migration): ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), ('type', models.CharField( - choices=[('GUILD_INVITE_ID', 'Guild Invite Id'), ('FILE_FORMAT', 'File Format'), - ('DOMAIN_NAME', 'Domain Name'), ('WORD_WATCHLIST', 'Word Watchlist')], + choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], help_text='The type of allowlist this is on.', max_length=50)), ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), ('content', models.TextField(help_text='The data to add to the allow or denylist.')), diff --git a/pydis_site/apps/api/models/bot/allow_deny_list.py b/pydis_site/apps/api/models/bot/allow_deny_list.py index b95dd72e..e1e09a78 100644 --- a/pydis_site/apps/api/models/bot/allow_deny_list.py +++ b/pydis_site/apps/api/models/bot/allow_deny_list.py @@ -8,10 +8,10 @@ class AllowDenyList(ModelTimestampMixin, ModelReprMixin, models.Model): AllowDenyListType = models.TextChoices( 'AllowDenyListType', - 'GUILD_INVITE_ID ' + 'GUILD_INVITE ' 'FILE_FORMAT ' 'DOMAIN_NAME ' - 'WORD_WATCHLIST ' + 'FILTER_TOKEN ' ) type = models.CharField( max_length=50, -- cgit v1.2.3 From 53e4dfef781d0cd8239122b2bb55e08ae1d031f5 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 19 Jul 2020 22:19:04 +0200 Subject: Document the get_types endpoint. --- pydis_site/apps/api/viewsets/bot/allow_deny_list.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/bot/allow_deny_list.py b/pydis_site/apps/api/viewsets/bot/allow_deny_list.py index 72cbc84c..6ea8da56 100644 --- a/pydis_site/apps/api/viewsets/bot/allow_deny_list.py +++ b/pydis_site/apps/api/viewsets/bot/allow_deny_list.py @@ -51,6 +51,20 @@ class AllowDenyListViewSet(ModelViewSet): - 200: returned on success - 404: returned if the id was not found. + ### GET /bot/allow_deny_lists/get_types + Returns a list of valid list types that can be used in POST requests. + + #### Response format + >>> [ + ... ["GUILD_INVITE","Guild Invite"], + ... ["FILE_FORMAT","File Format"], + ... ["DOMAIN_NAME","Domain Name"], + ... ["FILTER_TOKEN","Filter Token"] + ... ] + + #### Status codes + - 200: returned on success + ### POST /bot/allow_deny_lists Adds a single AllowDenyList item to the database. -- cgit v1.2.3 From 99fc3cde63b4f5aa84d2c1d6b9f5cd9f983fe6cb Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sun, 19 Jul 2020 22:24:10 +0200 Subject: Minor changes to tests, use subTest. --- pydis_site/apps/api/tests/test_allowlists.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_allowlists.py b/pydis_site/apps/api/tests/test_allowlists.py index 1440dc71..fd8772d0 100644 --- a/pydis_site/apps/api/tests/test_allowlists.py +++ b/pydis_site/apps/api/tests/test_allowlists.py @@ -80,8 +80,9 @@ class CreationTests(APISubdomainTestCase): cases = [{}, no_type_json, no_allowed_json, no_content_json] for case in cases: - response = self.client.post(URL, data=case) - self.assertEqual(response.status_code, 400) + with self.subTest(case=case): + response = self.client.post(URL, data=case) + self.assertEqual(response.status_code, 400) def test_returns_201_for_successful_creation(self): response = self.client.post(URL, data=JPEG_ALLOWLIST) @@ -96,17 +97,11 @@ class DeletionTests(APISubdomainTestCase): def test_deleting_unknown_id_returns_404(self): response = self.client.delete(f"{URL}/200") - self.assertEqual(response.status_code, 404) def test_deleting_known_id_returns_204(self): response = self.client.delete(f"{URL}/{self.jpeg_format.id}") - - self.assertEqual(response.status_code, 204) - - def test_name_gets_deleted(self): - response = self.client.delete(f"{URL}/{self.png_format.id}") self.assertEqual(response.status_code, 204) - response = self.client.get(f"{URL}/{self.png_format.id}") + response = self.client.get(f"{URL}/{self.jpeg_format.id}") self.assertNotIn(self.png_format.content, response.json()) -- cgit v1.2.3 From 285c81bc13e996246ad9463951853fc12903d766 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Mon, 27 Jul 2020 09:53:38 +0200 Subject: Rename AllowDenyList to FilterList --- .../0057_create_new_allowdenylist_model.py | 33 ------- .../migrations/0057_create_new_filterlist_model.py | 33 +++++++ pydis_site/apps/api/models/__init__.py | 2 +- pydis_site/apps/api/models/bot/__init__.py | 2 +- pydis_site/apps/api/models/bot/allow_deny_list.py | 41 -------- pydis_site/apps/api/models/bot/filter_list.py | 41 ++++++++ pydis_site/apps/api/serializers.py | 10 +- pydis_site/apps/api/tests/test_allowlists.py | 107 --------------------- pydis_site/apps/api/tests/test_filterlists.py | 107 +++++++++++++++++++++ pydis_site/apps/api/urls.py | 7 +- pydis_site/apps/api/viewsets/__init__.py | 2 +- pydis_site/apps/api/viewsets/bot/__init__.py | 2 +- .../apps/api/viewsets/bot/allow_deny_list.py | 97 ------------------- pydis_site/apps/api/viewsets/bot/filter_list.py | 97 +++++++++++++++++++ 14 files changed, 290 insertions(+), 291 deletions(-) delete mode 100644 pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py create mode 100644 pydis_site/apps/api/migrations/0057_create_new_filterlist_model.py delete mode 100644 pydis_site/apps/api/models/bot/allow_deny_list.py create mode 100644 pydis_site/apps/api/models/bot/filter_list.py delete mode 100644 pydis_site/apps/api/tests/test_allowlists.py create mode 100644 pydis_site/apps/api/tests/test_filterlists.py delete mode 100644 pydis_site/apps/api/viewsets/bot/allow_deny_list.py create mode 100644 pydis_site/apps/api/viewsets/bot/filter_list.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py b/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py deleted file mode 100644 index ab8c5f3f..00000000 --- a/pydis_site/apps/api/migrations/0057_create_new_allowdenylist_model.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0056_allow_blank_user_roles'), - ] - - operations = [ - migrations.CreateModel( - name='AllowDenyList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField( - choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), - ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], - help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allow or denylist.')), - ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.AddConstraint( - model_name='allowdenylist', - constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_allow_deny_list'), - ) - ] diff --git a/pydis_site/apps/api/migrations/0057_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0057_create_new_filterlist_model.py new file mode 100644 index 00000000..44025ee8 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_create_new_filterlist_model.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0056_allow_blank_user_roles'), + ] + + operations = [ + migrations.CreateModel( + name='FilterList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField( + choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], + help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='filterlist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list'), + ) + ] diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index 34973a8d..1d0ab7ea 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa from .bot import ( - AllowDenyList, + FilterList, BotSetting, DocumentationLink, DeletedMessage, diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index 1234b35a..efd98184 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -1,5 +1,5 @@ # flake8: noqa -from .allow_deny_list import AllowDenyList +from .filter_list import FilterList from .bot_setting import BotSetting from .deleted_message import DeletedMessage from .documentation_link import DocumentationLink diff --git a/pydis_site/apps/api/models/bot/allow_deny_list.py b/pydis_site/apps/api/models/bot/allow_deny_list.py deleted file mode 100644 index e1e09a78..00000000 --- a/pydis_site/apps/api/models/bot/allow_deny_list.py +++ /dev/null @@ -1,41 +0,0 @@ -from django.db import models - -from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin - - -class AllowDenyList(ModelTimestampMixin, ModelReprMixin, models.Model): - """An item that is either allowed or denied.""" - - AllowDenyListType = models.TextChoices( - 'AllowDenyListType', - 'GUILD_INVITE ' - 'FILE_FORMAT ' - 'DOMAIN_NAME ' - 'FILTER_TOKEN ' - ) - type = models.CharField( - max_length=50, - help_text="The type of allowlist this is on.", - choices=AllowDenyListType.choices, - ) - allowed = models.BooleanField( - help_text="Whether this item is on the allowlist or the denylist." - ) - content = models.TextField( - help_text="The data to add to the allow or denylist." - ) - comment = models.TextField( - help_text="Optional comment on this entry.", - null=True - ) - - class Meta: - """Metaconfig for this model.""" - - # This constraint ensures only one allow or denylist with the - # same content can exist. This means that we cannot have both an allow - # and a deny for the same item, and we cannot have duplicates of the - # same item. - constraints = [ - models.UniqueConstraint(fields=['content', 'type'], name='unique_allow_deny_list'), - ] diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py new file mode 100644 index 00000000..d279e137 --- /dev/null +++ b/pydis_site/apps/api/models/bot/filter_list.py @@ -0,0 +1,41 @@ +from django.db import models + +from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin + + +class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): + """An item that is either allowed or denied.""" + + FilterListType = models.TextChoices( + 'FilterListType', + 'GUILD_INVITE ' + 'FILE_FORMAT ' + 'DOMAIN_NAME ' + 'FILTER_TOKEN ' + ) + type = models.CharField( + max_length=50, + help_text="The type of allowlist this is on.", + choices=FilterListType.choices, + ) + allowed = models.BooleanField( + help_text="Whether this item is on the allowlist or the denylist." + ) + content = models.TextField( + help_text="The data to add to the allow or denylist." + ) + comment = models.TextField( + help_text="Optional comment on this entry.", + null=True + ) + + class Meta: + """Metaconfig for this model.""" + + # This constraint ensures only one filterlist with the + # same content can exist. This means that we cannot have both an allow + # and a deny for the same item, and we cannot have duplicates of the + # same item. + constraints = [ + models.UniqueConstraint(fields=['content', 'type'], name='unique_filter_list'), + ] diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 50f27f1d..8fd40da7 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -4,10 +4,10 @@ from rest_framework.validators import UniqueTogetherValidator from rest_framework_bulk import BulkSerializerMixin from .models import ( - AllowDenyList, BotSetting, DeletedMessage, DocumentationLink, + FilterList, Infraction, LogEntry, MessageDeletionContext, @@ -17,7 +17,7 @@ from .models import ( Reminder, Role, Tag, - User, + User ) @@ -104,13 +104,13 @@ class DocumentationLinkSerializer(ModelSerializer): fields = ('package', 'base_url', 'inventory_url') -class AllowDenyListSerializer(ModelSerializer): - """A class providing (de-)serialization of `AllowDenyList` instances.""" +class FilterListSerializer(ModelSerializer): + """A class providing (de-)serialization of `FilterList` instances.""" class Meta: """Metadata defined for the Django REST Framework.""" - model = AllowDenyList + model = FilterList fields = ('id', 'created_at', 'updated_at', 'type', 'allowed', 'content', 'comment') diff --git a/pydis_site/apps/api/tests/test_allowlists.py b/pydis_site/apps/api/tests/test_allowlists.py deleted file mode 100644 index fd8772d0..00000000 --- a/pydis_site/apps/api/tests/test_allowlists.py +++ /dev/null @@ -1,107 +0,0 @@ -from django_hosts.resolvers import reverse - -from pydis_site.apps.api.models import AllowDenyList -from pydis_site.apps.api.tests.base import APISubdomainTestCase - -URL = reverse('bot:allowdenylist-list', host='api') -JPEG_ALLOWLIST = { - "type": 'FILE_FORMAT', - "allowed": True, - "content": ".jpeg", -} -PNG_ALLOWLIST = { - "type": 'FILE_FORMAT', - "allowed": True, - "content": ".png", -} - - -class UnauthenticatedTests(APISubdomainTestCase): - def setUp(self): - super().setUp() - self.client.force_authenticate(user=None) - - def test_cannot_read_allowedlist_list(self): - response = self.client.get(URL) - - self.assertEqual(response.status_code, 401) - - -class EmptyDatabaseTests(APISubdomainTestCase): - def test_returns_empty_object(self): - response = self.client.get(URL) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) - - -class FetchTests(APISubdomainTestCase): - @classmethod - def setUpTestData(cls): - cls.jpeg_format = AllowDenyList.objects.create(**JPEG_ALLOWLIST) - cls.png_format = AllowDenyList.objects.create(**PNG_ALLOWLIST) - - def test_returns_name_in_list(self): - response = self.client.get(URL) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()[0]["content"], self.jpeg_format.content) - self.assertEqual(response.json()[1]["content"], self.png_format.content) - - def test_returns_single_item_by_id(self): - response = self.client.get(f'{URL}/{self.jpeg_format.id}') - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json().get("content"), self.jpeg_format.content) - - def test_returns_allow_deny_list_types(self): - response = self.client.get(f'{URL}/get_types') - - self.assertEqual(response.status_code, 200) - for api_type, model_type in zip(response.json(), AllowDenyList.AllowDenyListType.choices): - self.assertEquals(api_type[0], model_type[0]) - self.assertEquals(api_type[1], model_type[1]) - - -class CreationTests(APISubdomainTestCase): - def test_returns_400_for_missing_params(self): - no_type_json = { - "allowed": True, - "content": ".jpeg" - } - no_allowed_json = { - "type": "FILE_FORMAT", - "content": ".jpeg" - } - no_content_json = { - "allowed": True, - "type": "FILE_FORMAT" - } - cases = [{}, no_type_json, no_allowed_json, no_content_json] - - for case in cases: - with self.subTest(case=case): - response = self.client.post(URL, data=case) - self.assertEqual(response.status_code, 400) - - def test_returns_201_for_successful_creation(self): - response = self.client.post(URL, data=JPEG_ALLOWLIST) - self.assertEqual(response.status_code, 201) - - -class DeletionTests(APISubdomainTestCase): - @classmethod - def setUpTestData(cls): - cls.jpeg_format = AllowDenyList.objects.create(**JPEG_ALLOWLIST) - cls.png_format = AllowDenyList.objects.create(**PNG_ALLOWLIST) - - def test_deleting_unknown_id_returns_404(self): - response = self.client.delete(f"{URL}/200") - self.assertEqual(response.status_code, 404) - - def test_deleting_known_id_returns_204(self): - response = self.client.delete(f"{URL}/{self.jpeg_format.id}") - self.assertEqual(response.status_code, 204) - - response = self.client.get(f"{URL}/{self.jpeg_format.id}") - self.assertNotIn(self.png_format.content, response.json()) diff --git a/pydis_site/apps/api/tests/test_filterlists.py b/pydis_site/apps/api/tests/test_filterlists.py new file mode 100644 index 00000000..b577c31c --- /dev/null +++ b/pydis_site/apps/api/tests/test_filterlists.py @@ -0,0 +1,107 @@ +from django_hosts.resolvers import reverse + +from pydis_site.apps.api.models import FilterList +from pydis_site.apps.api.tests.base import APISubdomainTestCase + +URL = reverse('bot:filterlist-list', host='api') +JPEG_ALLOWLIST = { + "type": 'FILE_FORMAT', + "allowed": True, + "content": ".jpeg", +} +PNG_ALLOWLIST = { + "type": 'FILE_FORMAT', + "allowed": True, + "content": ".png", +} + + +class UnauthenticatedTests(APISubdomainTestCase): + def setUp(self): + super().setUp() + self.client.force_authenticate(user=None) + + def test_cannot_read_allowedlist_list(self): + response = self.client.get(URL) + + self.assertEqual(response.status_code, 401) + + +class EmptyDatabaseTests(APISubdomainTestCase): + def test_returns_empty_object(self): + response = self.client.get(URL) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), []) + + +class FetchTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + cls.jpeg_format = FilterList.objects.create(**JPEG_ALLOWLIST) + cls.png_format = FilterList.objects.create(**PNG_ALLOWLIST) + + def test_returns_name_in_list(self): + response = self.client.get(URL) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()[0]["content"], self.jpeg_format.content) + self.assertEqual(response.json()[1]["content"], self.png_format.content) + + def test_returns_single_item_by_id(self): + response = self.client.get(f'{URL}/{self.jpeg_format.id}') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json().get("content"), self.jpeg_format.content) + + def test_returns_filter_list_types(self): + response = self.client.get(f'{URL}/get-types') + + self.assertEqual(response.status_code, 200) + for api_type, model_type in zip(response.json(), FilterList.FilterListType.choices): + self.assertEquals(api_type[0], model_type[0]) + self.assertEquals(api_type[1], model_type[1]) + + +class CreationTests(APISubdomainTestCase): + def test_returns_400_for_missing_params(self): + no_type_json = { + "allowed": True, + "content": ".jpeg" + } + no_allowed_json = { + "type": "FILE_FORMAT", + "content": ".jpeg" + } + no_content_json = { + "allowed": True, + "type": "FILE_FORMAT" + } + cases = [{}, no_type_json, no_allowed_json, no_content_json] + + for case in cases: + with self.subTest(case=case): + response = self.client.post(URL, data=case) + self.assertEqual(response.status_code, 400) + + def test_returns_201_for_successful_creation(self): + response = self.client.post(URL, data=JPEG_ALLOWLIST) + self.assertEqual(response.status_code, 201) + + +class DeletionTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + cls.jpeg_format = FilterList.objects.create(**JPEG_ALLOWLIST) + cls.png_format = FilterList.objects.create(**PNG_ALLOWLIST) + + def test_deleting_unknown_id_returns_404(self): + response = self.client.delete(f"{URL}/200") + self.assertEqual(response.status_code, 404) + + def test_deleting_known_id_returns_204(self): + response = self.client.delete(f"{URL}/{self.jpeg_format.id}") + self.assertEqual(response.status_code, 204) + + response = self.client.get(f"{URL}/{self.jpeg_format.id}") + self.assertNotIn(self.png_format.content, response.json()) diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index b6ed2914..a4fd5b2e 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -3,10 +3,10 @@ from rest_framework.routers import DefaultRouter from .views import HealthcheckView, RulesView from .viewsets import ( - AllowDenyListViewSet, BotSettingViewSet, DeletedMessageViewSet, DocumentationLinkViewSet, + FilterListViewSet, InfractionViewSet, LogEntryViewSet, NominationViewSet, @@ -18,12 +18,11 @@ from .viewsets import ( UserViewSet ) - # http://www.django-rest-framework.org/api-guide/routers/#defaultrouter bot_router = DefaultRouter(trailing_slash=False) bot_router.register( - 'allow_deny_lists', - AllowDenyListViewSet + 'filter-lists', + FilterListViewSet ) bot_router.register( 'bot-settings', diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py index eb7d5098..8699517e 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa from .bot import ( - AllowDenyListViewSet, + FilterListViewSet, BotSettingViewSet, DeletedMessageViewSet, DocumentationLinkViewSet, diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index 11638dd8..e64e3988 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -1,5 +1,5 @@ # flake8: noqa -from .allow_deny_list import AllowDenyListViewSet +from .filter_list import FilterListViewSet from .bot_setting import BotSettingViewSet from .deleted_message import DeletedMessageViewSet from .documentation_link import DocumentationLinkViewSet diff --git a/pydis_site/apps/api/viewsets/bot/allow_deny_list.py b/pydis_site/apps/api/viewsets/bot/allow_deny_list.py deleted file mode 100644 index 6ea8da56..00000000 --- a/pydis_site/apps/api/viewsets/bot/allow_deny_list.py +++ /dev/null @@ -1,97 +0,0 @@ -from rest_framework.decorators import action -from rest_framework.request import Request -from rest_framework.response import Response -from rest_framework.viewsets import ModelViewSet - -from pydis_site.apps.api.models.bot.allow_deny_list import AllowDenyList -from pydis_site.apps.api.serializers import AllowDenyListSerializer - - -class AllowDenyListViewSet(ModelViewSet): - """ - View providing CRUD operations on items allowed or denied by our bot. - - ## Routes - ### GET /bot/allow_deny_lists - Returns all allow and denylist items in the database. - - #### Response format - >>> [ - ... { - ... 'id': "2309268224", - ... 'created_at': "01-01-2020 ...", - ... 'updated_at': "01-01-2020 ...", - ... 'type': "file_format", - ... 'allowed': 'true', - ... 'content': ".jpeg", - ... 'comment': "Popular image format.", - ... }, - ... ... - ... ] - - #### Status codes - - 200: returned on success - - 401: returned if unauthenticated - - ### GET /bot/allow_deny_lists/ - Returns a specific AllowDenyList item from the database. - - #### Response format - >>> { - ... 'id': "2309268224", - ... 'created_at': "01-01-2020 ...", - ... 'updated_at': "01-01-2020 ...", - ... 'type': "file_format", - ... 'allowed': 'true', - ... 'content': ".jpeg", - ... 'comment': "Popular image format.", - ... } - - #### Status codes - - 200: returned on success - - 404: returned if the id was not found. - - ### GET /bot/allow_deny_lists/get_types - Returns a list of valid list types that can be used in POST requests. - - #### Response format - >>> [ - ... ["GUILD_INVITE","Guild Invite"], - ... ["FILE_FORMAT","File Format"], - ... ["DOMAIN_NAME","Domain Name"], - ... ["FILTER_TOKEN","Filter Token"] - ... ] - - #### Status codes - - 200: returned on success - - ### POST /bot/allow_deny_lists - Adds a single AllowDenyList item to the database. - - #### Request body - >>> { - ... 'type': str, - ... 'allowed': bool, - ... 'content': str, - ... 'comment': Optional[str], - ... } - - #### Status codes - - 201: returned on success - - 400: if one of the given fields is invalid - - ### DELETE /bot/allow_deny_lists/ - Deletes the AllowDenyList item with the given `id`. - - #### Status codes - - 204: returned on success - - 404: if a tag with the given `id` does not exist - """ - - serializer_class = AllowDenyListSerializer - queryset = AllowDenyList.objects.all() - - @action(detail=False, methods=["get"]) - def get_types(self, _: Request) -> Response: - """Get a list of all the types of AllowDenyLists we support.""" - return Response(AllowDenyList.AllowDenyListType.choices) diff --git a/pydis_site/apps/api/viewsets/bot/filter_list.py b/pydis_site/apps/api/viewsets/bot/filter_list.py new file mode 100644 index 00000000..2cb21ab9 --- /dev/null +++ b/pydis_site/apps/api/viewsets/bot/filter_list.py @@ -0,0 +1,97 @@ +from rest_framework.decorators import action +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet + +from pydis_site.apps.api.models.bot.filter_list import FilterList +from pydis_site.apps.api.serializers import FilterListSerializer + + +class FilterListViewSet(ModelViewSet): + """ + View providing CRUD operations on items allowed or denied by our bot. + + ## Routes + ### GET /bot/filter-lists + Returns all filterlist items in the database. + + #### Response format + >>> [ + ... { + ... 'id': "2309268224", + ... 'created_at': "01-01-2020 ...", + ... 'updated_at': "01-01-2020 ...", + ... 'type': "file_format", + ... 'allowed': 'true', + ... 'content': ".jpeg", + ... 'comment': "Popular image format.", + ... }, + ... ... + ... ] + + #### Status codes + - 200: returned on success + - 401: returned if unauthenticated + + ### GET /bot/filter-lists/ + Returns a specific FilterList item from the database. + + #### Response format + >>> { + ... 'id': "2309268224", + ... 'created_at': "01-01-2020 ...", + ... 'updated_at': "01-01-2020 ...", + ... 'type': "file_format", + ... 'allowed': 'true', + ... 'content': ".jpeg", + ... 'comment': "Popular image format.", + ... } + + #### Status codes + - 200: returned on success + - 404: returned if the id was not found. + + ### GET /bot/filter-lists/get-types + Returns a list of valid list types that can be used in POST requests. + + #### Response format + >>> [ + ... ["GUILD_INVITE","Guild Invite"], + ... ["FILE_FORMAT","File Format"], + ... ["DOMAIN_NAME","Domain Name"], + ... ["FILTER_TOKEN","Filter Token"] + ... ] + + #### Status codes + - 200: returned on success + + ### POST /bot/filter-lists + Adds a single FilterList item to the database. + + #### Request body + >>> { + ... 'type': str, + ... 'allowed': bool, + ... 'content': str, + ... 'comment': Optional[str], + ... } + + #### Status codes + - 201: returned on success + - 400: if one of the given fields is invalid + + ### DELETE /bot/filter-lists/ + Deletes the FilterList item with the given `id`. + + #### Status codes + - 204: returned on success + - 404: if a tag with the given `id` does not exist + """ + + serializer_class = FilterListSerializer + queryset = FilterList.objects.all() + + @action(detail=False, url_path='get-types', methods=["get"]) + def get_types(self, _: Request) -> Response: + """Get a list of all the types of FilterLists we support.""" + return Response(FilterList.FilterListType.choices) -- cgit v1.2.3 From 0d7c95a80692f15c36b36152034e3bbe54e3c37c Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Wed, 29 Jul 2020 13:55:46 +0200 Subject: Handle unique validator in DRF, not Django. I was handling this in a Django vanilla kind of way, which was causing the constraint to return a 500 instead of a 400. This changes the approach to use the DRF way, and makes it return 400. It doesn't actually change the way anything behaves, other than returning the right status code. --- .../api/migrations/0057_create_new_filterlist_model.py | 4 ---- pydis_site/apps/api/models/bot/filter_list.py | 11 ----------- pydis_site/apps/api/serializers.py | 15 +++++++++++++++ 3 files changed, 15 insertions(+), 15 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0057_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0057_create_new_filterlist_model.py index 44025ee8..b5398568 100644 --- a/pydis_site/apps/api/migrations/0057_create_new_filterlist_model.py +++ b/pydis_site/apps/api/migrations/0057_create_new_filterlist_model.py @@ -25,9 +25,5 @@ class Migration(migrations.Migration): ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), ], bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.AddConstraint( - model_name='filterlist', - constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list'), ) ] diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py index d279e137..5961aed7 100644 --- a/pydis_site/apps/api/models/bot/filter_list.py +++ b/pydis_site/apps/api/models/bot/filter_list.py @@ -28,14 +28,3 @@ class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): help_text="Optional comment on this entry.", null=True ) - - class Meta: - """Metaconfig for this model.""" - - # This constraint ensures only one filterlist with the - # same content can exist. This means that we cannot have both an allow - # and a deny for the same item, and we cannot have duplicates of the - # same item. - constraints = [ - models.UniqueConstraint(fields=['content', 'type'], name='unique_filter_list'), - ] diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 8fd40da7..54bb8a5d 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -113,6 +113,21 @@ class FilterListSerializer(ModelSerializer): model = FilterList fields = ('id', 'created_at', 'updated_at', 'type', 'allowed', 'content', 'comment') + # This validator ensures only one filterlist with the + # same content can exist. This means that we cannot have both an allow + # and a deny for the same item, and we cannot have duplicates of the + # same item. + validators = [ + UniqueTogetherValidator( + queryset=FilterList.objects.all(), + fields=['content', 'type'], + message=( + "A filterlist for this item already exists. " + "Please note that you cannot add the same item to both allow and deny." + ) + ), + ] + class InfractionSerializer(ModelSerializer): """A class providing (de-)serialization of `Infraction` instances.""" -- cgit v1.2.3 From b4b99c5b56833409c68b89d807c9b9f6d385e7f3 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Wed, 29 Jul 2020 14:10:31 +0200 Subject: Add a test for checking duplicates. --- pydis_site/apps/api/tests/test_filterlists.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_filterlists.py b/pydis_site/apps/api/tests/test_filterlists.py index b577c31c..47aaaf31 100644 --- a/pydis_site/apps/api/tests/test_filterlists.py +++ b/pydis_site/apps/api/tests/test_filterlists.py @@ -88,6 +88,11 @@ class CreationTests(APISubdomainTestCase): response = self.client.post(URL, data=JPEG_ALLOWLIST) self.assertEqual(response.status_code, 201) + def test_returns_400_for_duplicate_creation(self): + self.client.post(URL, data=JPEG_ALLOWLIST) + response = self.client.post(URL, data=JPEG_ALLOWLIST) + self.assertEqual(response.status_code, 400) + class DeletionTests(APISubdomainTestCase): @classmethod -- cgit v1.2.3 From f15a345556b1902f3314d4edcc8ae3cf23da7a62 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Wed, 29 Jul 2020 18:19:15 +0200 Subject: Fix multiple leafs for migration graph. --- .../migrations/0057_create_new_filterlist_model.py | 29 ---------------------- .../migrations/0058_create_new_filterlist_model.py | 29 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 29 deletions(-) delete mode 100644 pydis_site/apps/api/migrations/0057_create_new_filterlist_model.py create mode 100644 pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0057_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0057_create_new_filterlist_model.py deleted file mode 100644 index b5398568..00000000 --- a/pydis_site/apps/api/migrations/0057_create_new_filterlist_model.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0056_allow_blank_user_roles'), - ] - - operations = [ - migrations.CreateModel( - name='FilterList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField( - choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), - ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], - help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allow or denylist.')), - ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ) - ] diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py new file mode 100644 index 00000000..fffb8159 --- /dev/null +++ b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py @@ -0,0 +1,29 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0057_merge_20200716_0751'), + ] + + operations = [ + migrations.CreateModel( + name='FilterList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField( + choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], + help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ) + ] -- cgit v1.2.3 From 12f447bc9636382a623e6b30ae43ccc036c488b5 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Wed, 29 Jul 2020 19:07:03 +0200 Subject: Add a constraint in the DB model as well. This really should've been handled automatically by DRF, and in the future, it will be. But for now, we need to have constraints both on the serializer (to get status code 400), and on the model (to prevent direct database constraint violations). See https://github.com/encode/django-rest-framework/issues/7173 --- .../apps/api/migrations/0058_create_new_filterlist_model.py | 4 ++++ pydis_site/apps/api/models/bot/filter_list.py | 11 +++++++++++ 2 files changed, 15 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py index fffb8159..aecfdad7 100644 --- a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py +++ b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py @@ -25,5 +25,9 @@ class Migration(migrations.Migration): ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), ], bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='filterlist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') ) ] diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py index 5961aed7..d279e137 100644 --- a/pydis_site/apps/api/models/bot/filter_list.py +++ b/pydis_site/apps/api/models/bot/filter_list.py @@ -28,3 +28,14 @@ class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): help_text="Optional comment on this entry.", null=True ) + + class Meta: + """Metaconfig for this model.""" + + # This constraint ensures only one filterlist with the + # same content can exist. This means that we cannot have both an allow + # and a deny for the same item, and we cannot have duplicates of the + # same item. + constraints = [ + models.UniqueConstraint(fields=['content', 'type'], name='unique_filter_list'), + ] -- cgit v1.2.3 From 82f2297fbdaa98e085655a9e66540980e3081210 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Thu, 30 Jul 2020 10:19:41 +0200 Subject: Provide callable, not return value. Co-authored-by: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> --- .../api/migrations/0025_allow_custom_inserted_at_infraction_field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py b/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py index 9589346d..c7fac012 100644 --- a/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py +++ b/pydis_site/apps/api/migrations/0025_allow_custom_inserted_at_infraction_field.py @@ -14,6 +14,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='infraction', name='inserted_at', - field=models.DateTimeField(default=timezone.now(), help_text='The date and time of the creation of this infraction.'), + field=models.DateTimeField(default=timezone.now, help_text='The date and time of the creation of this infraction.'), ), ] -- cgit v1.2.3 From 322004e10f023f197e5fd07b0a87e0a10f45ea35 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Thu, 30 Jul 2020 11:29:01 +0200 Subject: Add a migration for all the existing data. This will populate the table with domain names, guild invites, filter tokens and file formats. --- .../api/migrations/0059_populate_filterlists.py | 153 +++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0059_populate_filterlists.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py new file mode 100644 index 00000000..8c550191 --- /dev/null +++ b/pydis_site/apps/api/migrations/0059_populate_filterlists.py @@ -0,0 +1,153 @@ +from django.db import migrations + +guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +domain_name_blacklist = [ + ("pornhub.com", None, False), + ("liveleak.com", None, False), + ("grabify.link", None, False), + ("bmwforum.co", None, False), + ("leancoding.co", None, False), + ("spottyfly.com", None, False), + ("stopify.co", None, False), + ("yoütu.be", None, False), + ("discörd.com", None, False), + ("minecräft.com", None, False), + ("freegiftcards.co", None, False), + ("disçordapp.com", None, False), + ("fortnight.space", None, False), + ("fortnitechat.site", None, False), + ("joinmy.site", None, False), + ("curiouscat.club", None, False), + ("catsnthings.fun", None, False), + ("yourtube.site", None, False), + ("youtubeshort.watch", None, False), + ("catsnthing.com", None, False), + ("youtubeshort.pro", None, False), + ("canadianlumberjacks.online", None, False), + ("poweredbydialup.club", None, False), + ("poweredbydialup.online", None, False), + ("poweredbysecurity.org", None, False), + ("poweredbysecurity.online", None, False), + ("ssteam.site", None, False), + ("steamwalletgift.com", None, False), + ("discord.gift", None, False), + ("lmgtfy.com", None, False), +] + +filter_token_blacklist = [ + ("\bgoo+ks*\b", None, False), + ("\bky+s+\b", None, False), + ("\bki+ke+s*\b", None, False), + ("\bbeaner+s?\b", None, False), + ("\bcoo+ns*\b", None, False), + ("\bnig+lets*\b", None, False), + ("\bslant-eyes*\b", None, False), + ("\btowe?l-?head+s*\b", None, False), + ("\bchi*n+k+s*\b", None, False), + ("\bspick*s*\b", None, False), + ("\bkill* +(?:yo)?urself+\b", None, False), + ("\bjew+s*\b", None, False), + ("\bsuicide\b", None, False), + ("\brape\b", None, False), + ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), + ("\bta+r+d+\b", None, False), + ("\bcunts*\b", None, False), + ("\btrann*y\b", None, False), + ("\bshemale\b", None, False), + ("fa+g+s*", None, False), + ("卐", None, False), + ("卍", None, False), + ("࿖", None, False), + ("࿕", None, False), + ("࿘", None, False), + ("࿗", None, False), + ("cuck(?!oo+)", None, False), + ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), + ("fag+o+t+s*", None, False), +] + +file_format_whitelist = [ + (".3gp", None, True), + (".3g2", None, True), + (".avi", None, True), + (".bmp", None, True), + (".gif", None, True), + (".h264", None, True), + (".jpg", None, True), + (".jpeg", None, True), + (".m4v", None, True), + (".mkv", None, True), + (".mov", None, True), + (".mp4", None, True), + (".mpeg", None, True), + (".mpg", None, True), + (".png", None, True), + (".tiff", None, True), + (".wmv", None, True), + (".svg", None, True), + (".psd", "Photoshop", True), + (".ai", "Illustrator", True), + (".aep", "After Effects", True), + (".xcf", "GIMP", True), + (".mp3", None, True), + (".wav", None, True), + (".ogg", None, True), + (".webm", None, True), + (".webp", None, True), +] + +populate_data = { + "FILTER_TOKEN": filter_token_blacklist, + "DOMAIN_NAME": domain_name_blacklist, + "FILE_FORMAT": file_format_whitelist, + "GUILD_INVITE": guild_invite_whitelist, +} + + +class Migration(migrations.Migration): + dependencies = [("api", "0058_create_new_filterlist_model")] + + def populate_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + + for filterlist_type, metadata in populate_data.items(): + for content, comment, allowed in metadata: + FilterList.objects.create( + type=filterlist_type, + allowed=allowed, + content=content, + comment=comment, + ) + + def clear_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.all().delete() + + operations = [ + migrations.RunPython(populate_filterlists, clear_filterlists) + ] -- cgit v1.2.3 From de89a3c39c306274a75a41b528fa14155b00f392 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Thu, 30 Jul 2020 12:38:31 +0200 Subject: Delete FilterList objects for tests. Now that we have a migration that adds data, we can no longer have tests that operate on the assumption that the database is going to be empty. So, we're now clearing that table before these tests run. --- pydis_site/apps/api/tests/test_filterlists.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_filterlists.py b/pydis_site/apps/api/tests/test_filterlists.py index 47aaaf31..188c0fff 100644 --- a/pydis_site/apps/api/tests/test_filterlists.py +++ b/pydis_site/apps/api/tests/test_filterlists.py @@ -28,6 +28,10 @@ class UnauthenticatedTests(APISubdomainTestCase): class EmptyDatabaseTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + FilterList.objects.all().delete() + def test_returns_empty_object(self): response = self.client.get(URL) @@ -38,6 +42,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): class FetchTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): + FilterList.objects.all().delete() cls.jpeg_format = FilterList.objects.create(**JPEG_ALLOWLIST) cls.png_format = FilterList.objects.create(**PNG_ALLOWLIST) @@ -64,6 +69,10 @@ class FetchTests(APISubdomainTestCase): class CreationTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + FilterList.objects.all().delete() + def test_returns_400_for_missing_params(self): no_type_json = { "allowed": True, @@ -97,6 +106,7 @@ class CreationTests(APISubdomainTestCase): class DeletionTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): + FilterList.objects.all().delete() cls.jpeg_format = FilterList.objects.create(**JPEG_ALLOWLIST) cls.png_format = FilterList.objects.create(**PNG_ALLOWLIST) -- cgit v1.2.3 From e4cec0aeb2a791e622be8edd94fb4e82d150deab Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Mon, 3 Aug 2020 12:53:39 +0200 Subject: Fix bad migration 0059 This had the wrong data for GUILD_INVITES - it had invites instead of Guild IDs. This new migration solves this problem. --- .../migrations/0060_populate_filterlists_fix.py | 85 ++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py new file mode 100644 index 00000000..53846f02 --- /dev/null +++ b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py @@ -0,0 +1,85 @@ +from django.db import migrations + +bad_guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +guild_invite_whitelist = [ + ("267624335836053506", "Python Discord", True), + ("348658686962696195", "RLBot", True), + ("423249981340778496", "Kivy", True), + ("438622377094414346", "Pyglet", True), + ("524691714909274162", "Panda3D", True), + ("666560367173828639", "PyWeek", True), + ("702724176489873509", "Microsoft Python", True), + ("222078108977594368", "Discord.js Official", True), + ("238666723824238602", "Programming Discussions", True), + ("433980600391696384", "JetBrains Community", True), + ("204621105720328193", "Raspberry Pie", True), + ("286633898581164032", "Ren'Py", True), + ("440186186024222721", "Python Discord: Emojis 1", True), + ("578587418123304970", "Python Discord: Emojis 2", True), + ("159039020565790721", "Django", True), + ("273944235143593984", "STEM", True), + ("336642139381301249", "discord.py", True), + ("244230771232079873", "Programmers Hangout", True), + ("239433591950540801", "SpeakJS", True), + ("280033776820813825", "Functional Programming", True), + ("349505959032389632", "PyGame", True), + ("488751051629920277", "Python Atlanta", True), + ("143867839282020352", "C#", True), +] + + +class Migration(migrations.Migration): + dependencies = [("api", "0059_populate_filterlists")] + + def fix_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. + + for content, comment, allowed in guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + def restore_bad_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() + + for content, comment, allowed in bad_guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + operations = [ + migrations.RunPython(fix_filterlist, restore_bad_filterlist) + ] -- cgit v1.2.3 From 518a39b3e2b56f942328695d5e44674976317c7c Mon Sep 17 00:00:00 2001 From: Dennis Pham Date: Sat, 8 Aug 2020 14:46:04 -0400 Subject: Update rule 6 for the removal of #show-your-projects --- pydis_site/apps/api/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/views.py b/pydis_site/apps/api/views.py index a73d4718..7ac56641 100644 --- a/pydis_site/apps/api/views.py +++ b/pydis_site/apps/api/views.py @@ -140,7 +140,8 @@ class RulesView(APIView): ), ( "No spamming or unapproved advertising, including requests for paid work. " - "Open-source projects can be showcased in #show-your-projects." + "Open-source projects can be shared with others in #python-general and " + "code reviews can be asked for in a help channel." ), ( "Keep discussions relevant to channel topics and guidelines." -- cgit v1.2.3 From 4e67dbc2512653536de3eb603c537c8e40f058f6 Mon Sep 17 00:00:00 2001 From: Dennis Pham Date: Thu, 13 Aug 2020 18:22:14 -0400 Subject: Update Code Jam 7 to most recent in navbar --- pydis_site/templates/base/navbar.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/templates/base/navbar.html b/pydis_site/templates/base/navbar.html index f07662ae..c2915025 100644 --- a/pydis_site/templates/base/navbar.html +++ b/pydis_site/templates/base/navbar.html @@ -88,7 +88,7 @@ Events - Upcoming: Code Jam 7 + Most Recent: Code Jam 7 All events -- cgit v1.2.3 From 0ba6e25f88400e9a54ae9be2c176687ad69bb94f Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 20 Aug 2020 15:15:21 +0200 Subject: Allow direct fetching of reminders by id --- pydis_site/apps/api/tests/test_reminders.py | 28 ++++++++++++++++++++++++++++ pydis_site/apps/api/viewsets/bot/reminder.py | 8 +++++++- 2 files changed, 35 insertions(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_reminders.py b/pydis_site/apps/api/tests/test_reminders.py index a05d9296..9dffb668 100644 --- a/pydis_site/apps/api/tests/test_reminders.py +++ b/pydis_site/apps/api/tests/test_reminders.py @@ -163,6 +163,34 @@ class ReminderListTests(APISubdomainTestCase): self.assertEqual(response.json(), [self.rem_dict_one]) +class ReminderRetrieveTests(APISubdomainTestCase): + @classmethod + def setUpTestData(cls): + cls.author = User.objects.create( + id=6789, + name='Reminder author', + discriminator=6789, + ) + + cls.reminder = Reminder.objects.create( + author=cls.author, + content="Reminder content", + expiration=datetime.utcnow().isoformat(), + jump_url="http://example.com/", + channel_id=123 + ) + + def test_retrieve_unknown_returns_404(self): + url = reverse('bot:reminder-detail', args=("not_an_id",), host='api') + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + def test_retrieve_known_returns_200(self): + url = reverse('bot:reminder-detail', args=(self.reminder.id,), host='api') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + class ReminderUpdateTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py index 6f8a28f2..00413fb1 100644 --- a/pydis_site/apps/api/viewsets/bot/reminder.py +++ b/pydis_site/apps/api/viewsets/bot/reminder.py @@ -4,6 +4,7 @@ from rest_framework.mixins import ( CreateModelMixin, DestroyModelMixin, ListModelMixin, + RetrieveModelMixin, UpdateModelMixin ) from rest_framework.viewsets import GenericViewSet @@ -13,7 +14,12 @@ from pydis_site.apps.api.serializers import ReminderSerializer class ReminderViewSet( - CreateModelMixin, ListModelMixin, DestroyModelMixin, UpdateModelMixin, GenericViewSet + CreateModelMixin, + RetrieveModelMixin, + ListModelMixin, + DestroyModelMixin, + UpdateModelMixin, + GenericViewSet, ): """ View providing CRUD access to reminders. -- cgit v1.2.3 From 700de6b08b70f40dc97f3644d4e0ee32ab8f6ccf Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 20 Aug 2020 15:24:29 +0200 Subject: Update docstring for new fetching behaviour --- pydis_site/apps/api/viewsets/bot/reminder.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/bot/reminder.py b/pydis_site/apps/api/viewsets/bot/reminder.py index 00413fb1..111660d9 100644 --- a/pydis_site/apps/api/viewsets/bot/reminder.py +++ b/pydis_site/apps/api/viewsets/bot/reminder.py @@ -50,6 +50,30 @@ class ReminderViewSet( #### Status codes - 200: returned on success + ### GET /bot/reminders/ + Fetches the reminder with the given id. + + #### Response format + >>> + ... { + ... 'active': True, + ... 'author': 1020103901030, + ... 'mentions': [ + ... 336843820513755157, + ... 165023948638126080, + ... 267628507062992896 + ... ], + ... 'content': "Make dinner", + ... 'expiration': '5018-11-20T15:52:00Z', + ... 'id': 11, + ... 'channel_id': 634547009956872193, + ... 'jump_url': "https://discord.com/channels///" + ... } + + #### Status codes + - 200: returned on success + - 404: returned when the reminder doesn't exist + ### POST /bot/reminders Create a new reminder. -- cgit v1.2.3 From 1a55e92824d56a34a677c1f764a5393038e66955 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 22 Aug 2020 01:40:55 +0200 Subject: Add a context processor that outputs the git SHA. This will make the SHA available in all templates. --- pydis_site/context_processors.py | 10 ++++++++++ pydis_site/settings.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 pydis_site/context_processors.py (limited to 'pydis_site') diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py new file mode 100644 index 00000000..4ae0dbb3 --- /dev/null +++ b/pydis_site/context_processors.py @@ -0,0 +1,10 @@ +import git +from django.template import RequestContext + +REPO = git.Repo(search_parent_directories=True) +SHA = REPO.head.object.hexsha + + +def git_sha_processor(_: RequestContext) -> dict: + """Expose the git SHA for this repo to all views.""" + return {'git_sha': SHA} diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 2c87007c..afa097be 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -157,8 +157,8 @@ TEMPLATES = [ 'django.template.context_processors.static', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', - "sekizai.context_processors.sekizai", + "pydis_site.context_processors.git_sha_processor" ], }, }, -- cgit v1.2.3 From 75181a55977abe2eb6f3ac782b51bf13b8945024 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 22 Aug 2020 01:41:20 +0200 Subject: Add a comment with the git SHA to base templates. --- pydis_site/templates/base/base.html | 1 + pydis_site/templates/wiki/base.html | 1 + 2 files changed, 2 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/templates/base/base.html b/pydis_site/templates/base/base.html index 4c70d778..70426dc1 100644 --- a/pydis_site/templates/base/base.html +++ b/pydis_site/templates/base/base.html @@ -37,6 +37,7 @@ {% render_block "css" %} +
{% if messages %} diff --git a/pydis_site/templates/wiki/base.html b/pydis_site/templates/wiki/base.html index 846492ab..c9faf914 100644 --- a/pydis_site/templates/wiki/base.html +++ b/pydis_site/templates/wiki/base.html @@ -19,6 +19,7 @@ {% endblock %} {% block content %} + {% block site_navbar %} {% include "base/navbar.html" %} {% endblock %} -- cgit v1.2.3 From 60bc529e4b1cfa59e82ede102b88fca08f8f93b4 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 22 Aug 2020 02:04:44 +0200 Subject: Move git SHA initialization to __init__.py. This will make it easier to use in multiple places. --- pydis_site/__init__.py | 5 +++++ pydis_site/context_processors.py | 6 ++---- 2 files changed, 7 insertions(+), 4 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/__init__.py b/pydis_site/__init__.py index df67cf71..f0702577 100644 --- a/pydis_site/__init__.py +++ b/pydis_site/__init__.py @@ -1,3 +1,4 @@ +import git from wiki.plugins.macros.mdx import toc # Remove the toc header prefix. There's no option for this, so we gotta monkey patch it. @@ -7,3 +8,7 @@ toc.HEADER_ID_PREFIX = '' # by a string because Allauth won't let us just give it a list _there_, we have to point # at a list _somewhere else_ instead. VALIDATORS = [] + +# Git SHA +repo = git.Repo(search_parent_directories=True) +GIT_SHA = repo.head.object.hexsha diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py index 4ae0dbb3..bb66f21d 100644 --- a/pydis_site/context_processors.py +++ b/pydis_site/context_processors.py @@ -1,10 +1,8 @@ -import git from django.template import RequestContext -REPO = git.Repo(search_parent_directories=True) -SHA = REPO.head.object.hexsha +from pydis_site import GIT_SHA def git_sha_processor(_: RequestContext) -> dict: """Expose the git SHA for this repo to all views.""" - return {'git_sha': SHA} + return {'git_sha': GIT_SHA} -- cgit v1.2.3 From 212a06f400bfce95281e4c8d11baf6cb09e70861 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 22 Aug 2020 02:05:24 +0200 Subject: Set the sentry_sdk.init release to git_sha. This is the first step in implementing releases for Sentry. --- pydis_site/settings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/settings.py b/pydis_site/settings.py index afa097be..ff2942f0 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -20,6 +20,7 @@ import sentry_sdk from django.contrib.messages import constants as messages from sentry_sdk.integrations.django import DjangoIntegration +from pydis_site import GIT_SHA if typing.TYPE_CHECKING: from django.contrib.auth.models import User @@ -33,7 +34,8 @@ env = environ.Env( sentry_sdk.init( dsn=env('SITE_SENTRY_DSN'), integrations=[DjangoIntegration()], - send_default_pii=True + send_default_pii=True, + release=f"pydis-site@{GIT_SHA}" ) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) -- cgit v1.2.3 From 416a0874187fdf96346e3f504d6542b3214957db Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 22 Aug 2020 02:49:28 +0200 Subject: Move git SHA fetcher into utils. --- pydis_site/__init__.py | 5 ----- pydis_site/context_processors.py | 4 ++-- pydis_site/settings.py | 4 ++-- pydis_site/utils/resources.py | 10 ++++++++++ 4 files changed, 14 insertions(+), 9 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/__init__.py b/pydis_site/__init__.py index f0702577..df67cf71 100644 --- a/pydis_site/__init__.py +++ b/pydis_site/__init__.py @@ -1,4 +1,3 @@ -import git from wiki.plugins.macros.mdx import toc # Remove the toc header prefix. There's no option for this, so we gotta monkey patch it. @@ -8,7 +7,3 @@ toc.HEADER_ID_PREFIX = '' # by a string because Allauth won't let us just give it a list _there_, we have to point # at a list _somewhere else_ instead. VALIDATORS = [] - -# Git SHA -repo = git.Repo(search_parent_directories=True) -GIT_SHA = repo.head.object.hexsha diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py index bb66f21d..e905d9c7 100644 --- a/pydis_site/context_processors.py +++ b/pydis_site/context_processors.py @@ -1,8 +1,8 @@ from django.template import RequestContext -from pydis_site import GIT_SHA +from pydis_site.utils.resources import get_git_sha def git_sha_processor(_: RequestContext) -> dict: """Expose the git SHA for this repo to all views.""" - return {'git_sha': GIT_SHA} + return {'git_sha': get_git_sha()} diff --git a/pydis_site/settings.py b/pydis_site/settings.py index ff2942f0..e707a526 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -20,7 +20,7 @@ import sentry_sdk from django.contrib.messages import constants as messages from sentry_sdk.integrations.django import DjangoIntegration -from pydis_site import GIT_SHA +from pydis_site.utils.resources import get_git_sha if typing.TYPE_CHECKING: from django.contrib.auth.models import User @@ -35,7 +35,7 @@ sentry_sdk.init( dsn=env('SITE_SENTRY_DSN'), integrations=[DjangoIntegration()], send_default_pii=True, - release=f"pydis-site@{GIT_SHA}" + release=f"pydis-site@{get_git_sha()}" ) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) diff --git a/pydis_site/utils/resources.py b/pydis_site/utils/resources.py index 637fd785..d36c4b77 100644 --- a/pydis_site/utils/resources.py +++ b/pydis_site/utils/resources.py @@ -4,8 +4,13 @@ import glob import typing from dataclasses import dataclass +import git import yaml +# Git SHA +repo = git.Repo(search_parent_directories=True) +GIT_SHA = repo.head.object.hexsha + @dataclass class URL: @@ -89,3 +94,8 @@ def load_categories(order: typing.List[str]) -> typing.List[Category]: categories.append(Category.construct_from_directory(direc)) return categories + + +def get_git_sha() -> str: + """Get the Git SHA for this repo.""" + return GIT_SHA -- cgit v1.2.3 From 503bfba6855d3019a2739bb1adce69c457a8b26e Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 22 Aug 2020 03:18:25 +0200 Subject: Move git SHA fetcher into its own file. Fix tests. --- pydis_site/context_processors.py | 2 +- pydis_site/settings.py | 2 +- pydis_site/tests/test_utils.py | 11 +++++++++++ pydis_site/utils/__init__.py | 3 +++ pydis_site/utils/resources.py | 10 ---------- pydis_site/utils/utils.py | 10 ++++++++++ 6 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 pydis_site/tests/test_utils.py create mode 100644 pydis_site/utils/__init__.py create mode 100644 pydis_site/utils/utils.py (limited to 'pydis_site') diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py index e905d9c7..ab5a4168 100644 --- a/pydis_site/context_processors.py +++ b/pydis_site/context_processors.py @@ -1,6 +1,6 @@ from django.template import RequestContext -from pydis_site.utils.resources import get_git_sha +from pydis_site.utils import get_git_sha def git_sha_processor(_: RequestContext) -> dict: diff --git a/pydis_site/settings.py b/pydis_site/settings.py index e707a526..0a5b0eed 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -20,7 +20,7 @@ import sentry_sdk from django.contrib.messages import constants as messages from sentry_sdk.integrations.django import DjangoIntegration -from pydis_site.utils.resources import get_git_sha +from pydis_site.utils import get_git_sha if typing.TYPE_CHECKING: from django.contrib.auth.models import User diff --git a/pydis_site/tests/test_utils.py b/pydis_site/tests/test_utils.py new file mode 100644 index 00000000..f1419860 --- /dev/null +++ b/pydis_site/tests/test_utils.py @@ -0,0 +1,11 @@ +from django.test import TestCase + +from pydis_site.utils import get_git_sha +from pydis_site.utils.utils import GIT_SHA + + +class UtilsTests(TestCase): + + def test_git_sha(self): + """Test that the get_git_sha returns the correct SHA.""" + self.assertEqual(get_git_sha(), GIT_SHA) diff --git a/pydis_site/utils/__init__.py b/pydis_site/utils/__init__.py new file mode 100644 index 00000000..bb91b3d8 --- /dev/null +++ b/pydis_site/utils/__init__.py @@ -0,0 +1,3 @@ +from .utils import get_git_sha + +__all__ = ['get_git_sha'] diff --git a/pydis_site/utils/resources.py b/pydis_site/utils/resources.py index d36c4b77..637fd785 100644 --- a/pydis_site/utils/resources.py +++ b/pydis_site/utils/resources.py @@ -4,13 +4,8 @@ import glob import typing from dataclasses import dataclass -import git import yaml -# Git SHA -repo = git.Repo(search_parent_directories=True) -GIT_SHA = repo.head.object.hexsha - @dataclass class URL: @@ -94,8 +89,3 @@ def load_categories(order: typing.List[str]) -> typing.List[Category]: categories.append(Category.construct_from_directory(direc)) return categories - - -def get_git_sha() -> str: - """Get the Git SHA for this repo.""" - return GIT_SHA diff --git a/pydis_site/utils/utils.py b/pydis_site/utils/utils.py new file mode 100644 index 00000000..2033ea19 --- /dev/null +++ b/pydis_site/utils/utils.py @@ -0,0 +1,10 @@ +import git + +# Git SHA +repo = git.Repo(search_parent_directories=True) +GIT_SHA = repo.head.object.hexsha + + +def get_git_sha() -> str: + """Get the Git SHA for this repo.""" + return GIT_SHA -- cgit v1.2.3 From 88d364bfc809d20c6c4645abba75b889a526c804 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 22 Aug 2020 03:46:08 +0200 Subject: Remove the SHA from the wiki base.html This extends the regular base.html, so this would cause wiki pages to have two SHA's. --- pydis_site/templates/wiki/base.html | 1 - 1 file changed, 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/templates/wiki/base.html b/pydis_site/templates/wiki/base.html index c9faf914..846492ab 100644 --- a/pydis_site/templates/wiki/base.html +++ b/pydis_site/templates/wiki/base.html @@ -19,7 +19,6 @@ {% endblock %} {% block content %} - {% block site_navbar %} {% include "base/navbar.html" %} {% endblock %} -- cgit v1.2.3 From a71c38f07bba0191ba28eef12eea56e8a9265349 Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Sat, 22 Aug 2020 03:54:51 +0200 Subject: Move the SHA into constants.py. The util was redundant. Thanks @MarkKoz --- pydis_site/constants.py | 5 +++++ pydis_site/context_processors.py | 4 ++-- pydis_site/settings.py | 4 ++-- pydis_site/tests/test_utils.py | 11 ----------- pydis_site/utils/__init__.py | 3 --- pydis_site/utils/utils.py | 10 ---------- 6 files changed, 9 insertions(+), 28 deletions(-) create mode 100644 pydis_site/constants.py delete mode 100644 pydis_site/tests/test_utils.py delete mode 100644 pydis_site/utils/__init__.py delete mode 100644 pydis_site/utils/utils.py (limited to 'pydis_site') diff --git a/pydis_site/constants.py b/pydis_site/constants.py new file mode 100644 index 00000000..0b76694a --- /dev/null +++ b/pydis_site/constants.py @@ -0,0 +1,5 @@ +import git + +# Git SHA +repo = git.Repo(search_parent_directories=True) +GIT_SHA = repo.head.object.hexsha diff --git a/pydis_site/context_processors.py b/pydis_site/context_processors.py index ab5a4168..6937a3db 100644 --- a/pydis_site/context_processors.py +++ b/pydis_site/context_processors.py @@ -1,8 +1,8 @@ from django.template import RequestContext -from pydis_site.utils import get_git_sha +from pydis_site.constants import GIT_SHA def git_sha_processor(_: RequestContext) -> dict: """Expose the git SHA for this repo to all views.""" - return {'git_sha': get_git_sha()} + return {'git_sha': GIT_SHA} diff --git a/pydis_site/settings.py b/pydis_site/settings.py index 0a5b0eed..1f042c1b 100644 --- a/pydis_site/settings.py +++ b/pydis_site/settings.py @@ -20,7 +20,7 @@ import sentry_sdk from django.contrib.messages import constants as messages from sentry_sdk.integrations.django import DjangoIntegration -from pydis_site.utils import get_git_sha +from pydis_site.constants import GIT_SHA if typing.TYPE_CHECKING: from django.contrib.auth.models import User @@ -35,7 +35,7 @@ sentry_sdk.init( dsn=env('SITE_SENTRY_DSN'), integrations=[DjangoIntegration()], send_default_pii=True, - release=f"pydis-site@{get_git_sha()}" + release=f"pydis-site@{GIT_SHA}" ) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) diff --git a/pydis_site/tests/test_utils.py b/pydis_site/tests/test_utils.py deleted file mode 100644 index f1419860..00000000 --- a/pydis_site/tests/test_utils.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.test import TestCase - -from pydis_site.utils import get_git_sha -from pydis_site.utils.utils import GIT_SHA - - -class UtilsTests(TestCase): - - def test_git_sha(self): - """Test that the get_git_sha returns the correct SHA.""" - self.assertEqual(get_git_sha(), GIT_SHA) diff --git a/pydis_site/utils/__init__.py b/pydis_site/utils/__init__.py deleted file mode 100644 index bb91b3d8..00000000 --- a/pydis_site/utils/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .utils import get_git_sha - -__all__ = ['get_git_sha'] diff --git a/pydis_site/utils/utils.py b/pydis_site/utils/utils.py deleted file mode 100644 index 2033ea19..00000000 --- a/pydis_site/utils/utils.py +++ /dev/null @@ -1,10 +0,0 @@ -import git - -# Git SHA -repo = git.Repo(search_parent_directories=True) -GIT_SHA = repo.head.object.hexsha - - -def get_git_sha() -> str: - """Get the Git SHA for this repo.""" - return GIT_SHA -- cgit v1.2.3 From d52a20962bbda8f94646ff12c38eb71e2a5241ab Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:38:25 +0300 Subject: (Tag Cleanup): Removed Tags viewset. --- pydis_site/apps/api/viewsets/bot/__init__.py | 1 - pydis_site/apps/api/viewsets/bot/tag.py | 105 --------------------------- 2 files changed, 106 deletions(-) delete mode 100644 pydis_site/apps/api/viewsets/bot/tag.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/bot/__init__.py b/pydis_site/apps/api/viewsets/bot/__init__.py index e64e3988..84b87eab 100644 --- a/pydis_site/apps/api/viewsets/bot/__init__.py +++ b/pydis_site/apps/api/viewsets/bot/__init__.py @@ -9,5 +9,4 @@ from .off_topic_channel_name import OffTopicChannelNameViewSet from .offensive_message import OffensiveMessageViewSet from .reminder import ReminderViewSet from .role import RoleViewSet -from .tag import TagViewSet from .user import UserViewSet diff --git a/pydis_site/apps/api/viewsets/bot/tag.py b/pydis_site/apps/api/viewsets/bot/tag.py deleted file mode 100644 index 7e9ba117..00000000 --- a/pydis_site/apps/api/viewsets/bot/tag.py +++ /dev/null @@ -1,105 +0,0 @@ -from rest_framework.viewsets import ModelViewSet - -from pydis_site.apps.api.models.bot.tag import Tag -from pydis_site.apps.api.serializers import TagSerializer - - -class TagViewSet(ModelViewSet): - """ - View providing CRUD operations on tags shown by our bot. - - ## Routes - ### GET /bot/tags - Returns all tags in the database. - - #### Response format - >>> [ - ... { - ... 'title': "resources", - ... 'embed': { - ... 'content': "Did you really think I'd put something useful here?" - ... } - ... } - ... ] - - #### Status codes - - 200: returned on success - - ### GET /bot/tags/ - Gets a single tag by its title. - - #### Response format - >>> { - ... 'title': "My awesome tag", - ... 'embed': { - ... 'content': "totally not filler words" - ... } - ... } - - #### Status codes - - 200: returned on success - - 404: if a tag with the given `title` could not be found - - ### POST /bot/tags - Adds a single tag to the database. - - #### Request body - >>> { - ... 'title': str, - ... 'embed': dict - ... } - - The embed structure is the same as the embed structure that the Discord API - expects. You can view the documentation for it here: - https://discordapp.com/developers/docs/resources/channel#embed-object - - #### Status codes - - 201: returned on success - - 400: if one of the given fields is invalid - - ### PUT /bot/tags/ - Update the tag with the given `title`. - - #### Request body - >>> { - ... 'title': str, - ... 'embed': dict - ... } - - The embed structure is the same as the embed structure that the Discord API - expects. You can view the documentation for it here: - https://discordapp.com/developers/docs/resources/channel#embed-object - - #### Status codes - - 200: returned on success - - 400: if the request body was invalid, see response body for details - - 404: if the tag with the given `title` could not be found - - ### PATCH /bot/tags/ - Update the tag with the given `title`. - - #### Request body - >>> { - ... 'title': str, - ... 'embed': dict - ... } - - The embed structure is the same as the embed structure that the Discord API - expects. You can view the documentation for it here: - https://discordapp.com/developers/docs/resources/channel#embed-object - - #### Status codes - - 200: returned on success - - 400: if the request body was invalid, see response body for details - - 404: if the tag with the given `title` could not be found - - ### DELETE /bot/tags/ - Deletes the tag with the given `title`. - - #### Status codes - - 204: returned on success - - 404: if a tag with the given `title` does not exist - """ - - serializer_class = TagSerializer - queryset = Tag.objects.all() -- cgit v1.2.3 From 86409cf41469bd2e5b8d69988a729b3b88bb0a26 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:39:37 +0300 Subject: (Tag Cleanup): Removed Tags from Administration --- pydis_site/apps/api/admin.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/admin.py b/pydis_site/apps/api/admin.py index 0333fefc..dd1291b8 100644 --- a/pydis_site/apps/api/admin.py +++ b/pydis_site/apps/api/admin.py @@ -14,7 +14,6 @@ from .models import ( OffTopicChannelName, OffensiveMessage, Role, - Tag, User ) @@ -64,5 +63,4 @@ admin.site.register(Nomination) admin.site.register(OffensiveMessage) admin.site.register(OffTopicChannelName) admin.site.register(Role) -admin.site.register(Tag) admin.site.register(User) -- cgit v1.2.3 From 14453c9607ed83e040c9b1ed000a31e47568fd69 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:41:16 +0300 Subject: (Tag Cleanup): Removed Tags serializer. --- pydis_site/apps/api/serializers.py | 11 ----------- 1 file changed, 11 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/serializers.py b/pydis_site/apps/api/serializers.py index 52e0d972..90bd6f91 100644 --- a/pydis_site/apps/api/serializers.py +++ b/pydis_site/apps/api/serializers.py @@ -16,7 +16,6 @@ from .models import ( OffensiveMessage, Reminder, Role, - Tag, User ) @@ -250,16 +249,6 @@ class RoleSerializer(ModelSerializer): fields = ('id', 'name', 'colour', 'permissions', 'position') -class TagSerializer(ModelSerializer): - """A class providing (de-)serialization of `Tag` instances.""" - - class Meta: - """Metadata defined for the Django REST Framework.""" - - model = Tag - fields = ('title', 'embed') - - class UserSerializer(BulkSerializerMixin, ModelSerializer): """A class providing (de-)serialization of `User` instances.""" -- cgit v1.2.3 From 05b0c725b45dd87c755a8475d08442a0603a515e Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:41:58 +0300 Subject: (Tag Cleanup): Removed Tags URL --- pydis_site/apps/api/urls.py | 5 ----- 1 file changed, 5 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/urls.py b/pydis_site/apps/api/urls.py index a4fd5b2e..4dbf93db 100644 --- a/pydis_site/apps/api/urls.py +++ b/pydis_site/apps/api/urls.py @@ -14,7 +14,6 @@ from .viewsets import ( OffensiveMessageViewSet, ReminderViewSet, RoleViewSet, - TagViewSet, UserViewSet ) @@ -61,10 +60,6 @@ bot_router.register( 'roles', RoleViewSet ) -bot_router.register( - 'tags', - TagViewSet -) bot_router.register( 'users', UserViewSet -- cgit v1.2.3 From bb55875dcdaf838c41b1e3b2c3dc4fdf368b0f09 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:43:33 +0300 Subject: (Tag Cleanup): Removed Tags Model test. --- pydis_site/apps/api/tests/test_models.py | 5 ----- 1 file changed, 5 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py index e0e347bb..853e6621 100644 --- a/pydis_site/apps/api/tests/test_models.py +++ b/pydis_site/apps/api/tests/test_models.py @@ -14,7 +14,6 @@ from pydis_site.apps.api.models import ( OffensiveMessage, Reminder, Role, - Tag, User ) from pydis_site.apps.api.models.mixins import ModelReprMixin @@ -104,10 +103,6 @@ class StringDunderMethodTests(SimpleTestCase): ), creation=dt.utcnow() ), - Tag( - title='bob', - embed={'content': "the builder"} - ), User( id=5, name='bob', -- cgit v1.2.3 From 790c50cbc2d45134fc0b668bc21ca7e01e4af2a2 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:46:19 +0300 Subject: (Tag Cleanup): Removed Tags Model import from models __init__.py --- pydis_site/apps/api/models/bot/__init__.py | 1 - 1 file changed, 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/bot/__init__.py b/pydis_site/apps/api/models/bot/__init__.py index efd98184..1673b434 100644 --- a/pydis_site/apps/api/models/bot/__init__.py +++ b/pydis_site/apps/api/models/bot/__init__.py @@ -11,5 +11,4 @@ from .off_topic_channel_name import OffTopicChannelName from .offensive_message import OffensiveMessage from .reminder import Reminder from .role import Role -from .tag import Tag from .user import User -- cgit v1.2.3 From b16809620f444872f4d295ee55e4e8e9cdde12c8 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:49:49 +0300 Subject: (Tag Cleanup): Moved embed validators from Tag model to utils.py --- pydis_site/apps/api/models/mixins.py | 173 +++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py index 5d75b78b..2d658cb9 100644 --- a/pydis_site/apps/api/models/mixins.py +++ b/pydis_site/apps/api/models/mixins.py @@ -1,4 +1,177 @@ +from collections.abc import Mapping from operator import itemgetter +from typing import Any, Dict + +from django.core.exceptions import ValidationError +from django.core.validators import MaxLengthValidator, MinLengthValidator + + +def is_bool_validator(value: Any) -> None: + """Validates if a given value is of type bool.""" + if not isinstance(value, bool): + raise ValidationError(f"This field must be of type bool, not {type(value)}.") + + +def validate_tag_embed_fields(fields: dict) -> None: + """Raises a ValidationError if any of the given embed fields is invalid.""" + field_validators = { + 'name': (MaxLengthValidator(limit_value=256),), + 'value': (MaxLengthValidator(limit_value=1024),), + 'inline': (is_bool_validator,), + } + + required_fields = ('name', 'value') + + for field in fields: + if not isinstance(field, Mapping): + raise ValidationError("Embed fields must be a mapping.") + + if not all(required_field in field for required_field in required_fields): + raise ValidationError( + f"Embed fields must contain the following fields: {', '.join(required_fields)}." + ) + + for field_name, value in field.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed field field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_tag_embed_footer(footer: Dict[str, str]) -> None: + """Raises a ValidationError if the given footer is invalid.""" + field_validators = { + 'text': ( + MinLengthValidator( + limit_value=1, + message="Footer text must not be empty." + ), + MaxLengthValidator(limit_value=2048) + ), + 'icon_url': (), + 'proxy_icon_url': () + } + + if not isinstance(footer, Mapping): + raise ValidationError("Embed footer must be a mapping.") + + for field_name, value in footer.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed footer field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_tag_embed_author(author: Any) -> None: + """Raises a ValidationError if the given author is invalid.""" + field_validators = { + 'name': ( + MinLengthValidator( + limit_value=1, + message="Embed author name must not be empty." + ), + MaxLengthValidator(limit_value=256) + ), + 'url': (), + 'icon_url': (), + 'proxy_icon_url': () + } + + if not isinstance(author, Mapping): + raise ValidationError("Embed author must be a mapping.") + + for field_name, value in author.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed author field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_tag_embed(embed: Any) -> None: + """ + Validate a JSON document containing an embed as possible to send on Discord. + + This attempts to rebuild the validation used by Discord + as well as possible by checking for various embed limits so we can + ensure that any embed we store here will also be accepted as a + valid embed by the Discord API. + + Using this directly is possible, although not intended - you usually + stick this onto the `validators` keyword argument of model fields. + + Example: + + >>> from django.contrib.postgres import fields as pgfields + >>> from django.db import models + >>> from pydis_site.apps.api.models.bot.tag import validate_tag_embed + >>> class MyMessage(models.Model): + ... embed = pgfields.JSONField( + ... validators=( + ... validate_tag_embed, + ... ) + ... ) + ... # ... + ... + + Args: + embed (Any): + A dictionary describing the contents of this embed. + See the official documentation for a full reference + of accepted keys by this dictionary: + https://discordapp.com/developers/docs/resources/channel#embed-object + + Raises: + ValidationError: + In case the given embed is deemed invalid, a `ValidationError` + is raised which in turn will allow Django to display errors + as appropriate. + """ + all_keys = { + 'title', 'type', 'description', 'url', 'timestamp', + 'color', 'footer', 'image', 'thumbnail', 'video', + 'provider', 'author', 'fields' + } + one_required_of = {'description', 'fields', 'image', 'title', 'video'} + field_validators = { + 'title': ( + MinLengthValidator( + limit_value=1, + message="Embed title must not be empty." + ), + MaxLengthValidator(limit_value=256) + ), + 'description': (MaxLengthValidator(limit_value=2048),), + 'fields': ( + MaxLengthValidator(limit_value=25), + validate_tag_embed_fields + ), + 'footer': (validate_tag_embed_footer,), + 'author': (validate_tag_embed_author,) + } + + if not embed: + raise ValidationError("Tag embed must not be empty.") + + elif not isinstance(embed, Mapping): + raise ValidationError("Tag embed must be a mapping.") + + elif not any(field in embed for field in one_required_of): + raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.") + + for required_key in one_required_of: + if required_key in embed and not embed[required_key]: + raise ValidationError(f"Key {required_key!r} must not be empty.") + + for field_name, value in embed.items(): + if field_name not in all_keys: + raise ValidationError(f"Unknown field name: {field_name!r}") + + if field_name in field_validators: + for validator in field_validators[field_name]: + validator(value) from django.db import models -- cgit v1.2.3 From 1f623b6629bdbc854323dc71d149a647355031c6 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:52:05 +0300 Subject: (Tag Cleanup): Removed `tag` from embed validator names due Tags will be removed. --- pydis_site/apps/api/models/mixins.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py index 2d658cb9..e95888b7 100644 --- a/pydis_site/apps/api/models/mixins.py +++ b/pydis_site/apps/api/models/mixins.py @@ -12,7 +12,7 @@ def is_bool_validator(value: Any) -> None: raise ValidationError(f"This field must be of type bool, not {type(value)}.") -def validate_tag_embed_fields(fields: dict) -> None: +def validate_embed_fields(fields: dict) -> None: """Raises a ValidationError if any of the given embed fields is invalid.""" field_validators = { 'name': (MaxLengthValidator(limit_value=256),), @@ -39,7 +39,7 @@ def validate_tag_embed_fields(fields: dict) -> None: validator(value) -def validate_tag_embed_footer(footer: Dict[str, str]) -> None: +def validate_embed_footer(footer: Dict[str, str]) -> None: """Raises a ValidationError if the given footer is invalid.""" field_validators = { 'text': ( @@ -64,7 +64,7 @@ def validate_tag_embed_footer(footer: Dict[str, str]) -> None: validator(value) -def validate_tag_embed_author(author: Any) -> None: +def validate_embed_author(author: Any) -> None: """Raises a ValidationError if the given author is invalid.""" field_validators = { 'name': ( @@ -90,7 +90,7 @@ def validate_tag_embed_author(author: Any) -> None: validator(value) -def validate_tag_embed(embed: Any) -> None: +def validate_embed(embed: Any) -> None: """ Validate a JSON document containing an embed as possible to send on Discord. @@ -106,11 +106,11 @@ def validate_tag_embed(embed: Any) -> None: >>> from django.contrib.postgres import fields as pgfields >>> from django.db import models - >>> from pydis_site.apps.api.models.bot.tag import validate_tag_embed + >>> from pydis_site.apps.api.models.utils import validate_embed >>> class MyMessage(models.Model): ... embed = pgfields.JSONField( ... validators=( - ... validate_tag_embed, + ... validate_embed, ... ) ... ) ... # ... @@ -146,10 +146,10 @@ def validate_tag_embed(embed: Any) -> None: 'description': (MaxLengthValidator(limit_value=2048),), 'fields': ( MaxLengthValidator(limit_value=25), - validate_tag_embed_fields + validate_embed_fields ), - 'footer': (validate_tag_embed_footer,), - 'author': (validate_tag_embed_author,) + 'footer': (validate_embed_footer,), + 'author': (validate_embed_author,) } if not embed: -- cgit v1.2.3 From 9b6b9d9d01811bbe2d9a1964970e3b4a38180623 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:55:09 +0300 Subject: (Tag Cleanup): Replaced import from Tags model with utils.py validator import. --- pydis_site/apps/api/models/bot/message.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index 78dcbf1d..e929ea25 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -5,9 +5,8 @@ from django.core.validators import MinValueValidator from django.db import models from django.utils import timezone -from pydis_site.apps.api.models.bot.tag import validate_tag_embed from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.mixins import ModelReprMixin +from pydis_site.apps.api.models.utils import ModelReprMixin, validate_embed class Message(ModelReprMixin, models.Model): @@ -47,7 +46,7 @@ class Message(ModelReprMixin, models.Model): ) embeds = pgfields.ArrayField( pgfields.JSONField( - validators=(validate_tag_embed,) + validators=(validate_embed,) ), blank=True, help_text="Embeds attached to this message." -- cgit v1.2.3 From 1029c56eb7e1f986a540a58bc2e657db6925a93a Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 08:57:27 +0300 Subject: (Tag Cleanup): Replaced import from Tags model with utils.py validator import in validators test. --- pydis_site/apps/api/tests/test_validators.py | 56 ++++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_validators.py b/pydis_site/apps/api/tests/test_validators.py index 241af08c..8bb7b917 100644 --- a/pydis_site/apps/api/tests/test_validators.py +++ b/pydis_site/apps/api/tests/test_validators.py @@ -5,7 +5,7 @@ from django.test import TestCase from ..models.bot.bot_setting import validate_bot_setting_name from ..models.bot.offensive_message import future_date_validator -from ..models.bot.tag import validate_tag_embed +from ..models.utils import validate_embed REQUIRED_KEYS = ( @@ -25,77 +25,77 @@ class BotSettingValidatorTests(TestCase): class TagEmbedValidatorTests(TestCase): def test_rejects_non_mapping(self): with self.assertRaises(ValidationError): - validate_tag_embed('non-empty non-mapping') + validate_embed('non-empty non-mapping') def test_rejects_missing_required_keys(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'unknown': "key" }) def test_rejects_one_correct_one_incorrect(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'provider': "??", 'title': "" }) def test_rejects_empty_required_key(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': '' }) def test_rejects_list_as_embed(self): with self.assertRaises(ValidationError): - validate_tag_embed([]) + validate_embed([]) def test_rejects_required_keys_and_unknown_keys(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': "the duck walked up to the lemonade stand", 'and': "he said to the man running the stand" }) def test_rejects_too_long_title(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': 'a' * 257 }) def test_rejects_too_many_fields(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'fields': [{} for _ in range(26)] }) def test_rejects_too_long_description(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'description': 'd' * 2049 }) def test_allows_valid_embed(self): - validate_tag_embed({ + validate_embed({ 'title': "My embed", 'description': "look at my embed, my embed is amazing" }) def test_allows_unvalidated_fields(self): - validate_tag_embed({ + validate_embed({ 'title': "My embed", 'provider': "what am I??" }) def test_rejects_fields_as_list_of_non_mappings(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'fields': ['abc'] }) def test_rejects_fields_with_unknown_fields(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'fields': [ { 'what': "is this field" @@ -105,7 +105,7 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_fields_with_too_long_name(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'fields': [ { 'name': "a" * 257 @@ -115,7 +115,7 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_one_correct_one_incorrect_field(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'fields': [ { 'name': "Totally valid", @@ -131,7 +131,7 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_missing_required_field_field(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'fields': [ { 'name': "Totally valid", @@ -142,7 +142,7 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_invalid_inline_field_field(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'fields': [ { 'name': "Totally valid", @@ -153,7 +153,7 @@ class TagEmbedValidatorTests(TestCase): }) def test_allows_valid_fields(self): - validate_tag_embed({ + validate_embed({ 'fields': [ { 'name': "valid", @@ -174,14 +174,14 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_footer_as_non_mapping(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'footer': [] }) def test_rejects_footer_with_unknown_fields(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'footer': { 'duck': "quack" @@ -190,7 +190,7 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_footer_with_empty_text(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'footer': { 'text': "" @@ -198,7 +198,7 @@ class TagEmbedValidatorTests(TestCase): }) def test_allows_footer_with_proper_values(self): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'footer': { 'text': "django good" @@ -207,14 +207,14 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_author_as_non_mapping(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'author': [] }) def test_rejects_author_with_unknown_field(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'author': { 'field': "that is unknown" @@ -223,7 +223,7 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_author_with_empty_name(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'author': { 'name': "" @@ -232,7 +232,7 @@ class TagEmbedValidatorTests(TestCase): def test_rejects_author_with_one_correct_one_incorrect(self): with self.assertRaises(ValidationError): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'author': { # Relies on "dictionary insertion order remembering" (D.I.O.R.) behaviour @@ -242,7 +242,7 @@ class TagEmbedValidatorTests(TestCase): }) def test_allows_author_with_proper_values(self): - validate_tag_embed({ + validate_embed({ 'title': "whatever", 'author': { 'name': "Bob" -- cgit v1.2.3 From 5df2d613bc72aad60fed73d2703dff55093def03 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 09:02:48 +0300 Subject: (Tag Cleanup): Removed tag import in models __init__.py --- pydis_site/apps/api/models/__init__.py | 1 - 1 file changed, 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/__init__.py b/pydis_site/apps/api/models/__init__.py index 1d0ab7ea..e3f928e1 100644 --- a/pydis_site/apps/api/models/__init__.py +++ b/pydis_site/apps/api/models/__init__.py @@ -12,7 +12,6 @@ from .bot import ( OffTopicChannelName, Reminder, Role, - Tag, User ) from .log_entry import LogEntry -- cgit v1.2.3 From 494b735f762bf1780448db4591a4be4850dcdd4a Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 09:17:43 +0300 Subject: (Tag Cleanup): Removed Tag model --- .../apps/api/migrations/0019_deletedmessage.py | 2 +- pydis_site/apps/api/models/bot/tag.py | 25 ---------------------- 2 files changed, 1 insertion(+), 26 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0019_deletedmessage.py b/pydis_site/apps/api/migrations/0019_deletedmessage.py index 33746253..6b848d64 100644 --- a/pydis_site/apps/api/migrations/0019_deletedmessage.py +++ b/pydis_site/apps/api/migrations/0019_deletedmessage.py @@ -18,7 +18,7 @@ class Migration(migrations.Migration): ('id', models.BigIntegerField(help_text='The message ID as taken from Discord.', primary_key=True, serialize=False, validators=[django.core.validators.MinValueValidator(limit_value=0, message='Message IDs cannot be negative.')])), ('channel_id', models.BigIntegerField(help_text='The channel ID that this message was sent in, taken from Discord.', validators=[django.core.validators.MinValueValidator(limit_value=0, message='Channel IDs cannot be negative.')])), ('content', models.CharField(help_text='The content of this message, taken from Discord.', max_length=2000)), - ('embeds', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]), help_text='Embeds attached to this message.', size=None)), + ('embeds', django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.utils.validate_embed]), help_text='Embeds attached to this message.', size=None)), ('author', models.ForeignKey(help_text='The author of this message.', on_delete=django.db.models.deletion.CASCADE, to='api.User')), ('deletion_context', models.ForeignKey(help_text='The deletion context this message is part of.', on_delete=django.db.models.deletion.CASCADE, to='api.MessageDeletionContext')), ], diff --git a/pydis_site/apps/api/models/bot/tag.py b/pydis_site/apps/api/models/bot/tag.py index 5e53582f..790ad37a 100644 --- a/pydis_site/apps/api/models/bot/tag.py +++ b/pydis_site/apps/api/models/bot/tag.py @@ -1,12 +1,8 @@ from collections.abc import Mapping from typing import Any, Dict -from django.contrib.postgres import fields as pgfields from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, MinLengthValidator -from django.db import models - -from pydis_site.apps.api.models.mixins import ModelReprMixin def is_bool_validator(value: Any) -> None: @@ -175,24 +171,3 @@ def validate_tag_embed(embed: Any) -> None: if field_name in field_validators: for validator in field_validators[field_name]: validator(value) - - -class Tag(ModelReprMixin, models.Model): - """A tag providing (hopefully) useful information.""" - - title = models.CharField( - max_length=100, - help_text=( - "The title of this tag, shown in searches and providing " - "a quick overview over what this embed contains." - ), - primary_key=True - ) - embed = pgfields.JSONField( - help_text="The actual embed shown by this tag.", - validators=(validate_tag_embed,) - ) - - def __str__(self): - """Returns the title of this tag, for display purposes.""" - return self.title -- cgit v1.2.3 From ad5183a3956321c8956defa3f32e399400662bf6 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 09:20:48 +0300 Subject: (Tag Cleanup): Removed unnecessary tag validation migration. --- pydis_site/apps/api/migrations/0008_tag_embed_validator.py | 7 ------- 1 file changed, 7 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0008_tag_embed_validator.py b/pydis_site/apps/api/migrations/0008_tag_embed_validator.py index d53ddb90..d92042d2 100644 --- a/pydis_site/apps/api/migrations/0008_tag_embed_validator.py +++ b/pydis_site/apps/api/migrations/0008_tag_embed_validator.py @@ -1,7 +1,5 @@ # Generated by Django 2.1.1 on 2018-09-23 10:07 -import pydis_site.apps.api.models.bot.tag -import django.contrib.postgres.fields.jsonb from django.db import migrations @@ -12,9 +10,4 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AlterField( - model_name='tag', - name='embed', - field=django.contrib.postgres.fields.jsonb.JSONField(help_text='The actual embed shown by this tag.', validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]), - ), ] -- cgit v1.2.3 From a11d3a38a03b63a2018785917a56fe54c1b1f027 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 09:21:17 +0300 Subject: (Tag Cleanup): Removed Tag model file. --- pydis_site/apps/api/models/bot/tag.py | 173 ---------------------------------- 1 file changed, 173 deletions(-) delete mode 100644 pydis_site/apps/api/models/bot/tag.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/bot/tag.py b/pydis_site/apps/api/models/bot/tag.py deleted file mode 100644 index 790ad37a..00000000 --- a/pydis_site/apps/api/models/bot/tag.py +++ /dev/null @@ -1,173 +0,0 @@ -from collections.abc import Mapping -from typing import Any, Dict - -from django.core.exceptions import ValidationError -from django.core.validators import MaxLengthValidator, MinLengthValidator - - -def is_bool_validator(value: Any) -> None: - """Validates if a given value is of type bool.""" - if not isinstance(value, bool): - raise ValidationError(f"This field must be of type bool, not {type(value)}.") - - -def validate_tag_embed_fields(fields: dict) -> None: - """Raises a ValidationError if any of the given embed fields is invalid.""" - field_validators = { - 'name': (MaxLengthValidator(limit_value=256),), - 'value': (MaxLengthValidator(limit_value=1024),), - 'inline': (is_bool_validator,), - } - - required_fields = ('name', 'value') - - for field in fields: - if not isinstance(field, Mapping): - raise ValidationError("Embed fields must be a mapping.") - - if not all(required_field in field for required_field in required_fields): - raise ValidationError( - f"Embed fields must contain the following fields: {', '.join(required_fields)}." - ) - - for field_name, value in field.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed field field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_tag_embed_footer(footer: Dict[str, str]) -> None: - """Raises a ValidationError if the given footer is invalid.""" - field_validators = { - 'text': ( - MinLengthValidator( - limit_value=1, - message="Footer text must not be empty." - ), - MaxLengthValidator(limit_value=2048) - ), - 'icon_url': (), - 'proxy_icon_url': () - } - - if not isinstance(footer, Mapping): - raise ValidationError("Embed footer must be a mapping.") - - for field_name, value in footer.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed footer field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_tag_embed_author(author: Any) -> None: - """Raises a ValidationError if the given author is invalid.""" - field_validators = { - 'name': ( - MinLengthValidator( - limit_value=1, - message="Embed author name must not be empty." - ), - MaxLengthValidator(limit_value=256) - ), - 'url': (), - 'icon_url': (), - 'proxy_icon_url': () - } - - if not isinstance(author, Mapping): - raise ValidationError("Embed author must be a mapping.") - - for field_name, value in author.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed author field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_tag_embed(embed: Any) -> None: - """ - Validate a JSON document containing an embed as possible to send on Discord. - - This attempts to rebuild the validation used by Discord - as well as possible by checking for various embed limits so we can - ensure that any embed we store here will also be accepted as a - valid embed by the Discord API. - - Using this directly is possible, although not intended - you usually - stick this onto the `validators` keyword argument of model fields. - - Example: - - >>> from django.contrib.postgres import fields as pgfields - >>> from django.db import models - >>> from pydis_site.apps.api.models.bot.tag import validate_tag_embed - >>> class MyMessage(models.Model): - ... embed = pgfields.JSONField( - ... validators=( - ... validate_tag_embed, - ... ) - ... ) - ... # ... - ... - - Args: - embed (Any): - A dictionary describing the contents of this embed. - See the official documentation for a full reference - of accepted keys by this dictionary: - https://discordapp.com/developers/docs/resources/channel#embed-object - - Raises: - ValidationError: - In case the given embed is deemed invalid, a `ValidationError` - is raised which in turn will allow Django to display errors - as appropriate. - """ - all_keys = { - 'title', 'type', 'description', 'url', 'timestamp', - 'color', 'footer', 'image', 'thumbnail', 'video', - 'provider', 'author', 'fields' - } - one_required_of = {'description', 'fields', 'image', 'title', 'video'} - field_validators = { - 'title': ( - MinLengthValidator( - limit_value=1, - message="Embed title must not be empty." - ), - MaxLengthValidator(limit_value=256) - ), - 'description': (MaxLengthValidator(limit_value=2048),), - 'fields': ( - MaxLengthValidator(limit_value=25), - validate_tag_embed_fields - ), - 'footer': (validate_tag_embed_footer,), - 'author': (validate_tag_embed_author,) - } - - if not embed: - raise ValidationError("Tag embed must not be empty.") - - elif not isinstance(embed, Mapping): - raise ValidationError("Tag embed must be a mapping.") - - elif not any(field in embed for field in one_required_of): - raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.") - - for required_key in one_required_of: - if required_key in embed and not embed[required_key]: - raise ValidationError(f"Key {required_key!r} must not be empty.") - - for field_name, value in embed.items(): - if field_name not in all_keys: - raise ValidationError(f"Unknown field name: {field_name!r}") - - if field_name in field_validators: - for validator in field_validators[field_name]: - validator(value) -- cgit v1.2.3 From 5357d5baf3234550c09f88fa38a9f436503f570c Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 09:21:48 +0300 Subject: (Tag Cleanup): Added Tag removal migration --- pydis_site/apps/api/migrations/0051_delete_tag.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0051_delete_tag.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0051_delete_tag.py b/pydis_site/apps/api/migrations/0051_delete_tag.py new file mode 100644 index 00000000..bada5788 --- /dev/null +++ b/pydis_site/apps/api/migrations/0051_delete_tag.py @@ -0,0 +1,16 @@ +# Generated by Django 2.2.11 on 2020-04-01 06:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0050_remove_infractions_active_default_value'), + ] + + operations = [ + migrations.DeleteModel( + name='Tag', + ), + ] -- cgit v1.2.3 From 99485facba10d1f0d918e8c0ab152372db89c7b9 Mon Sep 17 00:00:00 2001 From: ks123 Date: Wed, 1 Apr 2020 09:25:15 +0300 Subject: (Tag Cleanup): Removed Tag viewset from viewsets __init__.py --- pydis_site/apps/api/viewsets/__init__.py | 1 - 1 file changed, 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/__init__.py b/pydis_site/apps/api/viewsets/__init__.py index 8699517e..dfbb880d 100644 --- a/pydis_site/apps/api/viewsets/__init__.py +++ b/pydis_site/apps/api/viewsets/__init__.py @@ -10,7 +10,6 @@ from .bot import ( OffTopicChannelNameViewSet, ReminderViewSet, RoleViewSet, - TagViewSet, UserViewSet ) from .log_entry import LogEntryViewSet -- cgit v1.2.3 From 2b2f491e476c4564899c8716fde189da4a493287 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 27 Aug 2020 07:34:41 +0300 Subject: Move import to beginning of models mixins file --- pydis_site/apps/api/models/mixins.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py index e95888b7..692d14f7 100644 --- a/pydis_site/apps/api/models/mixins.py +++ b/pydis_site/apps/api/models/mixins.py @@ -4,6 +4,7 @@ from typing import Any, Dict from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, MinLengthValidator +from django.db import models def is_bool_validator(value: Any) -> None: @@ -173,8 +174,6 @@ def validate_embed(embed: Any) -> None: for validator in field_validators[field_name]: validator(value) -from django.db import models - class ModelReprMixin: """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" -- cgit v1.2.3 From 2011d2f2c30dc67c1f34e82e5593f8e4ce1243dd Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 04:44:08 +0000 Subject: Move some parts from Mixins file to utils --- pydis_site/apps/api/models/mixins.py | 172 ---------------------------------- pydis_site/apps/api/models/utils.py | 174 +++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 172 deletions(-) create mode 100644 pydis_site/apps/api/models/utils.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py index 692d14f7..5d75b78b 100644 --- a/pydis_site/apps/api/models/mixins.py +++ b/pydis_site/apps/api/models/mixins.py @@ -1,180 +1,8 @@ -from collections.abc import Mapping from operator import itemgetter -from typing import Any, Dict -from django.core.exceptions import ValidationError -from django.core.validators import MaxLengthValidator, MinLengthValidator from django.db import models -def is_bool_validator(value: Any) -> None: - """Validates if a given value is of type bool.""" - if not isinstance(value, bool): - raise ValidationError(f"This field must be of type bool, not {type(value)}.") - - -def validate_embed_fields(fields: dict) -> None: - """Raises a ValidationError if any of the given embed fields is invalid.""" - field_validators = { - 'name': (MaxLengthValidator(limit_value=256),), - 'value': (MaxLengthValidator(limit_value=1024),), - 'inline': (is_bool_validator,), - } - - required_fields = ('name', 'value') - - for field in fields: - if not isinstance(field, Mapping): - raise ValidationError("Embed fields must be a mapping.") - - if not all(required_field in field for required_field in required_fields): - raise ValidationError( - f"Embed fields must contain the following fields: {', '.join(required_fields)}." - ) - - for field_name, value in field.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed field field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_embed_footer(footer: Dict[str, str]) -> None: - """Raises a ValidationError if the given footer is invalid.""" - field_validators = { - 'text': ( - MinLengthValidator( - limit_value=1, - message="Footer text must not be empty." - ), - MaxLengthValidator(limit_value=2048) - ), - 'icon_url': (), - 'proxy_icon_url': () - } - - if not isinstance(footer, Mapping): - raise ValidationError("Embed footer must be a mapping.") - - for field_name, value in footer.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed footer field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_embed_author(author: Any) -> None: - """Raises a ValidationError if the given author is invalid.""" - field_validators = { - 'name': ( - MinLengthValidator( - limit_value=1, - message="Embed author name must not be empty." - ), - MaxLengthValidator(limit_value=256) - ), - 'url': (), - 'icon_url': (), - 'proxy_icon_url': () - } - - if not isinstance(author, Mapping): - raise ValidationError("Embed author must be a mapping.") - - for field_name, value in author.items(): - if field_name not in field_validators: - raise ValidationError(f"Unknown embed author field: {field_name!r}.") - - for validator in field_validators[field_name]: - validator(value) - - -def validate_embed(embed: Any) -> None: - """ - Validate a JSON document containing an embed as possible to send on Discord. - - This attempts to rebuild the validation used by Discord - as well as possible by checking for various embed limits so we can - ensure that any embed we store here will also be accepted as a - valid embed by the Discord API. - - Using this directly is possible, although not intended - you usually - stick this onto the `validators` keyword argument of model fields. - - Example: - - >>> from django.contrib.postgres import fields as pgfields - >>> from django.db import models - >>> from pydis_site.apps.api.models.utils import validate_embed - >>> class MyMessage(models.Model): - ... embed = pgfields.JSONField( - ... validators=( - ... validate_embed, - ... ) - ... ) - ... # ... - ... - - Args: - embed (Any): - A dictionary describing the contents of this embed. - See the official documentation for a full reference - of accepted keys by this dictionary: - https://discordapp.com/developers/docs/resources/channel#embed-object - - Raises: - ValidationError: - In case the given embed is deemed invalid, a `ValidationError` - is raised which in turn will allow Django to display errors - as appropriate. - """ - all_keys = { - 'title', 'type', 'description', 'url', 'timestamp', - 'color', 'footer', 'image', 'thumbnail', 'video', - 'provider', 'author', 'fields' - } - one_required_of = {'description', 'fields', 'image', 'title', 'video'} - field_validators = { - 'title': ( - MinLengthValidator( - limit_value=1, - message="Embed title must not be empty." - ), - MaxLengthValidator(limit_value=256) - ), - 'description': (MaxLengthValidator(limit_value=2048),), - 'fields': ( - MaxLengthValidator(limit_value=25), - validate_embed_fields - ), - 'footer': (validate_embed_footer,), - 'author': (validate_embed_author,) - } - - if not embed: - raise ValidationError("Tag embed must not be empty.") - - elif not isinstance(embed, Mapping): - raise ValidationError("Tag embed must be a mapping.") - - elif not any(field in embed for field in one_required_of): - raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.") - - for required_key in one_required_of: - if required_key in embed and not embed[required_key]: - raise ValidationError(f"Key {required_key!r} must not be empty.") - - for field_name, value in embed.items(): - if field_name not in all_keys: - raise ValidationError(f"Unknown field name: {field_name!r}") - - if field_name in field_validators: - for validator in field_validators[field_name]: - validator(value) - - class ModelReprMixin: """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py new file mode 100644 index 00000000..97a79507 --- /dev/null +++ b/pydis_site/apps/api/models/utils.py @@ -0,0 +1,174 @@ +from collections.abc import Mapping +from operator import itemgetter +from typing import Any, Dict + +from django.core.exceptions import ValidationError +from django.core.validators import MaxLengthValidator, MinLengthValidator + + +def is_bool_validator(value: Any) -> None: + """Validates if a given value is of type bool.""" + if not isinstance(value, bool): + raise ValidationError(f"This field must be of type bool, not {type(value)}.") + + +def validate_embed_fields(fields: dict) -> None: + """Raises a ValidationError if any of the given embed fields is invalid.""" + field_validators = { + 'name': (MaxLengthValidator(limit_value=256),), + 'value': (MaxLengthValidator(limit_value=1024),), + 'inline': (is_bool_validator,), + } + + required_fields = ('name', 'value') + + for field in fields: + if not isinstance(field, Mapping): + raise ValidationError("Embed fields must be a mapping.") + + if not all(required_field in field for required_field in required_fields): + raise ValidationError( + f"Embed fields must contain the following fields: {', '.join(required_fields)}." + ) + + for field_name, value in field.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed field field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_embed_footer(footer: Dict[str, str]) -> None: + """Raises a ValidationError if the given footer is invalid.""" + field_validators = { + 'text': ( + MinLengthValidator( + limit_value=1, + message="Footer text must not be empty." + ), + MaxLengthValidator(limit_value=2048) + ), + 'icon_url': (), + 'proxy_icon_url': () + } + + if not isinstance(footer, Mapping): + raise ValidationError("Embed footer must be a mapping.") + + for field_name, value in footer.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed footer field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_embed_author(author: Any) -> None: + """Raises a ValidationError if the given author is invalid.""" + field_validators = { + 'name': ( + MinLengthValidator( + limit_value=1, + message="Embed author name must not be empty." + ), + MaxLengthValidator(limit_value=256) + ), + 'url': (), + 'icon_url': (), + 'proxy_icon_url': () + } + + if not isinstance(author, Mapping): + raise ValidationError("Embed author must be a mapping.") + + for field_name, value in author.items(): + if field_name not in field_validators: + raise ValidationError(f"Unknown embed author field: {field_name!r}.") + + for validator in field_validators[field_name]: + validator(value) + + +def validate_embed(embed: Any) -> None: + """ + Validate a JSON document containing an embed as possible to send on Discord. + + This attempts to rebuild the validation used by Discord + as well as possible by checking for various embed limits so we can + ensure that any embed we store here will also be accepted as a + valid embed by the Discord API. + + Using this directly is possible, although not intended - you usually + stick this onto the `validators` keyword argument of model fields. + + Example: + + >>> from django.contrib.postgres import fields as pgfields + >>> from django.db import models + >>> from pydis_site.apps.api.models.utils import validate_embed + >>> class MyMessage(models.Model): + ... embed = pgfields.JSONField( + ... validators=( + ... validate_embed, + ... ) + ... ) + ... # ... + ... + + Args: + embed (Any): + A dictionary describing the contents of this embed. + See the official documentation for a full reference + of accepted keys by this dictionary: + https://discordapp.com/developers/docs/resources/channel#embed-object + + Raises: + ValidationError: + In case the given embed is deemed invalid, a `ValidationError` + is raised which in turn will allow Django to display errors + as appropriate. + """ + all_keys = { + 'title', 'type', 'description', 'url', 'timestamp', + 'color', 'footer', 'image', 'thumbnail', 'video', + 'provider', 'author', 'fields' + } + one_required_of = {'description', 'fields', 'image', 'title', 'video'} + field_validators = { + 'title': ( + MinLengthValidator( + limit_value=1, + message="Embed title must not be empty." + ), + MaxLengthValidator(limit_value=256) + ), + 'description': (MaxLengthValidator(limit_value=2048),), + 'fields': ( + MaxLengthValidator(limit_value=25), + validate_embed_fields + ), + 'footer': (validate_embed_footer,), + 'author': (validate_embed_author,) + } + + if not embed: + raise ValidationError("Tag embed must not be empty.") + + elif not isinstance(embed, Mapping): + raise ValidationError("Tag embed must be a mapping.") + + elif not any(field in embed for field in one_required_of): + raise ValidationError(f"Tag embed must contain one of the fields {one_required_of}.") + + for required_key in one_required_of: + if required_key in embed and not embed[required_key]: + raise ValidationError(f"Key {required_key!r} must not be empty.") + + for field_name, value in embed.items(): + if field_name not in all_keys: + raise ValidationError(f"Unknown field name: {field_name!r}") + + if field_name in field_validators: + for validator in field_validators[field_name]: + validator(value) -- cgit v1.2.3 From 1ab4ccbbbcf38dddace5855ed9521219821b7698 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 04:48:09 +0000 Subject: Remove unused import from models utils --- pydis_site/apps/api/models/utils.py | 1 - 1 file changed, 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py index 97a79507..107231ba 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/utils.py @@ -1,5 +1,4 @@ from collections.abc import Mapping -from operator import itemgetter from typing import Any, Dict from django.core.exceptions import ValidationError -- cgit v1.2.3 From fd2909c936efb9ab2285896b297b405c4dd7a1fb Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 05:35:01 +0000 Subject: Move last parts from mixins to utils and delete mixins --- pydis_site/apps/api/models/mixins.py | 31 ------------------------------- pydis_site/apps/api/models/utils.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 31 deletions(-) delete mode 100644 pydis_site/apps/api/models/mixins.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py deleted file mode 100644 index 5d75b78b..00000000 --- a/pydis_site/apps/api/models/mixins.py +++ /dev/null @@ -1,31 +0,0 @@ -from operator import itemgetter - -from django.db import models - - -class ModelReprMixin: - """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" - - def __repr__(self): - """Returns the current model class name and initialisation parameters.""" - attributes = ' '.join( - f'{attribute}={value!r}' - for attribute, value in sorted( - self.__dict__.items(), - key=itemgetter(0) - ) - if not attribute.startswith('_') - ) - return f'<{self.__class__.__name__}({attributes})>' - - -class ModelTimestampMixin(models.Model): - """Mixin providing created_at and updated_at fields.""" - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - """Metaconfig for the mixin.""" - - abstract = True diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py index 107231ba..692d14f7 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/utils.py @@ -1,8 +1,10 @@ from collections.abc import Mapping +from operator import itemgetter from typing import Any, Dict from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, MinLengthValidator +from django.db import models def is_bool_validator(value: Any) -> None: @@ -171,3 +173,31 @@ def validate_embed(embed: Any) -> None: if field_name in field_validators: for validator in field_validators[field_name]: validator(value) + + +class ModelReprMixin: + """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" + + def __repr__(self): + """Returns the current model class name and initialisation parameters.""" + attributes = ' '.join( + f'{attribute}={value!r}' + for attribute, value in sorted( + self.__dict__.items(), + key=itemgetter(0) + ) + if not attribute.startswith('_') + ) + return f'<{self.__class__.__name__}({attributes})>' + + +class ModelTimestampMixin(models.Model): + """Mixin providing created_at and updated_at fields.""" + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + """Metaconfig for the mixin.""" + + abstract = True -- cgit v1.2.3 From 4eeb841724e7603dffc5334a6e4aa7b7b7ced997 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 05:37:55 +0000 Subject: Fix FilterList model mixins import path --- pydis_site/apps/api/models/bot/filter_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py index d279e137..cb4acb68 100644 --- a/pydis_site/apps/api/models/bot/filter_list.py +++ b/pydis_site/apps/api/models/bot/filter_list.py @@ -1,6 +1,6 @@ from django.db import models -from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin +from pydis_site.apps.api.models.utils import ModelReprMixin, ModelTimestampMixin class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): -- cgit v1.2.3 From feafa0950d9132350e36f58fbae57121363d3277 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 05:47:47 +0000 Subject: Still move mixins back to its own file --- pydis_site/apps/api/models/bot/filter_list.py | 2 +- pydis_site/apps/api/models/mixins.py | 31 +++++++++++++++++++++++++++ pydis_site/apps/api/models/utils.py | 30 -------------------------- 3 files changed, 32 insertions(+), 31 deletions(-) create mode 100644 pydis_site/apps/api/models/mixins.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/bot/filter_list.py b/pydis_site/apps/api/models/bot/filter_list.py index cb4acb68..d279e137 100644 --- a/pydis_site/apps/api/models/bot/filter_list.py +++ b/pydis_site/apps/api/models/bot/filter_list.py @@ -1,6 +1,6 @@ from django.db import models -from pydis_site.apps.api.models.utils import ModelReprMixin, ModelTimestampMixin +from pydis_site.apps.api.models.mixins import ModelReprMixin, ModelTimestampMixin class FilterList(ModelTimestampMixin, ModelReprMixin, models.Model): diff --git a/pydis_site/apps/api/models/mixins.py b/pydis_site/apps/api/models/mixins.py new file mode 100644 index 00000000..5d75b78b --- /dev/null +++ b/pydis_site/apps/api/models/mixins.py @@ -0,0 +1,31 @@ +from operator import itemgetter + +from django.db import models + + +class ModelReprMixin: + """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" + + def __repr__(self): + """Returns the current model class name and initialisation parameters.""" + attributes = ' '.join( + f'{attribute}={value!r}' + for attribute, value in sorted( + self.__dict__.items(), + key=itemgetter(0) + ) + if not attribute.startswith('_') + ) + return f'<{self.__class__.__name__}({attributes})>' + + +class ModelTimestampMixin(models.Model): + """Mixin providing created_at and updated_at fields.""" + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + """Metaconfig for the mixin.""" + + abstract = True diff --git a/pydis_site/apps/api/models/utils.py b/pydis_site/apps/api/models/utils.py index 692d14f7..107231ba 100644 --- a/pydis_site/apps/api/models/utils.py +++ b/pydis_site/apps/api/models/utils.py @@ -1,10 +1,8 @@ from collections.abc import Mapping -from operator import itemgetter from typing import Any, Dict from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator, MinLengthValidator -from django.db import models def is_bool_validator(value: Any) -> None: @@ -173,31 +171,3 @@ def validate_embed(embed: Any) -> None: if field_name in field_validators: for validator in field_validators[field_name]: validator(value) - - -class ModelReprMixin: - """Mixin providing a `__repr__()` to display model class name and initialisation parameters.""" - - def __repr__(self): - """Returns the current model class name and initialisation parameters.""" - attributes = ' '.join( - f'{attribute}={value!r}' - for attribute, value in sorted( - self.__dict__.items(), - key=itemgetter(0) - ) - if not attribute.startswith('_') - ) - return f'<{self.__class__.__name__}({attributes})>' - - -class ModelTimestampMixin(models.Model): - """Mixin providing created_at and updated_at fields.""" - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - """Metaconfig for the mixin.""" - - abstract = True -- cgit v1.2.3 From e1f75feb74c24b262b276534070e4ffb3bf295b3 Mon Sep 17 00:00:00 2001 From: Karlis S Date: Thu, 27 Aug 2020 05:53:09 +0000 Subject: Fix import paths of mixins in message model --- pydis_site/apps/api/models/bot/message.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index e929ea25..f6ae55a5 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -6,7 +6,8 @@ from django.db import models from django.utils import timezone from pydis_site.apps.api.models.bot.user import User -from pydis_site.apps.api.models.utils import ModelReprMixin, validate_embed +from pydis_site.apps.api.models.mixins import ModelReprMixin +from pydis_site.apps.api.models.utils import validate_embed class Message(ModelReprMixin, models.Model): -- cgit v1.2.3 From ec689ad42d28de55d47f9d3730389ae7e179d565 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 27 Aug 2020 08:57:28 +0300 Subject: Fix embed validator location in migration --- pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py index e617e1c9..b03c8a18 100644 --- a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py +++ b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py @@ -16,6 +16,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='deletedmessage', name='embeds', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.bot.tag.validate_tag_embed]), blank=True, help_text='Embeds attached to this message.', size=None), + field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.jsonb.JSONField(validators=[pydis_site.apps.api.models.utils.validate_embed]), blank=True, help_text='Embeds attached to this message.', size=None), ), ] -- cgit v1.2.3 From b43477faa59970adffc93c58743a4abce53b1547 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 27 Aug 2020 09:01:28 +0300 Subject: Replace bad import on migration --- pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py index b03c8a18..124c6a57 100644 --- a/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py +++ b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py @@ -3,7 +3,7 @@ import django.contrib.postgres.fields import django.contrib.postgres.fields.jsonb from django.db import migrations -import pydis_site.apps.api.models.bot.tag +import pydis_site.apps.api.models.utils class Migration(migrations.Migration): -- cgit v1.2.3 From 32b76b192d72f4235a64b8024131988b4e4c0c36 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 29 Aug 2020 08:55:57 +0300 Subject: Simplify non-random off-topic names selection --- .../api/viewsets/bot/off_topic_channel_name.py | 39 ++++++++++------------ 1 file changed, 17 insertions(+), 22 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 9af69ae4..29978015 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -1,3 +1,4 @@ +from django.db.models import Case, When, Value from django.db.models.query import QuerySet from django.http.request import HttpRequest from django.shortcuts import get_object_or_404 @@ -108,28 +109,22 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) - queryset = self.get_queryset().order_by('?').exclude(used=True)[:random_count] - self.get_queryset().filter( - name__in=(query.name for query in queryset) - ).update(used=True) - - # When the client requests more channel names than are available, - # we reset all names to used=False and start a new round of names. - if len(queryset) < random_count: - # Figure out how many additional names we need, and don't fetch duplicate names. - names_needed = random_count - len(queryset) - other_names = self.get_queryset().order_by('?').exclude( - name__in=(query.name for query in queryset) - )[:names_needed] - - # Reset the `used` field to False for all names except the ones we just used. - self.get_queryset().exclude(name__in=( - query.name for query in other_names) - ).update(used=False) - - # Join original queryset (that had missing names) - # and extension with these missing names. - queryset = list(queryset) + list(other_names) + queryset = self.get_queryset().order_by('used', '?')[:random_count] + + # When any name is used in our listing then this means we reached end of round + # and we need to reset all other names `used` to False + if any(offtopic_name.used for offtopic_name in queryset): + self.get_queryset().update( + used=Case( # These names that we just got have to be excluded from updating to False + When(name__in=(offtopic_name.name for offtopic_name in queryset), then=Value(True)), + default=Value(False) + ) + ) + else: + # Otherwise mark selected names `used` to True + self.get_queryset().filter( + name__in=(offtopic_name.name for offtopic_name in queryset) + ).update(used=True) serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From d5945b3d82124214d1bbbf8e156f3516fd3a70cc Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 29 Aug 2020 09:06:11 +0300 Subject: Fix off-topic names test docstrings mood --- .../apps/api/tests/test_off_topic_channel_names.py | 30 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index cac9405a..3ab8b22d 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -10,14 +10,14 @@ class UnauthenticatedTests(APISubdomainTestCase): self.client.force_authenticate(user=None) def test_cannot_read_off_topic_channel_name_list(self): - """Test does this return 401 response code when not authenticated.""" + """Return a 401 response when not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_cannot_read_off_topic_channel_name_list_with_random_item_param(self): - """Test does this give 401 code when `random_items` provided and not authenticated.""" + """Return a 401 response when `random_items` provided and not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=no') @@ -26,7 +26,7 @@ class UnauthenticatedTests(APISubdomainTestCase): class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_empty_object(self): - """Test does this return empty list when no names in database.""" + """Return empty list when no names in database.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -34,7 +34,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_empty_list_with_get_all_param(self): - """Test does this return empty list when no names and `random_items` param provided.""" + """Return empty list when no names and `random_items` param provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=5') @@ -42,7 +42,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_400_for_bad_random_items_param(self): - """Test does this return error message when passing not integer as `random_items`.""" + """Return error message when passing not integer as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=totally-a-valid-integer') @@ -52,7 +52,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): }) def test_returns_400_for_negative_random_items_param(self): - """Test does this return error message when passing negative int as `random_items`.""" + """Return error message when passing negative int as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=-5') @@ -69,7 +69,7 @@ class ListTests(APISubdomainTestCase): cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True) def test_returns_name_in_list(self): - """Test does this return all off-topic channel names.""" + """Return all off-topic channel names.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -83,7 +83,7 @@ class ListTests(APISubdomainTestCase): ) def test_returns_single_item_with_random_items_param_set_to_1(self): - """Test does this return not-used name instead used.""" + """Return not-used name instead used.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=1') @@ -92,7 +92,7 @@ class ListTests(APISubdomainTestCase): self.assertEqual(response.json(), [self.test_name.name]) def test_running_out_of_names_with_random_parameter(self): - """Test does this reset names `used` parameter to `False` when running out of names.""" + """Reset names `used` parameter to `False` when running out of names.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=2') @@ -110,7 +110,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_201_for_unicode_chars(self): - """Test does this accept all valid characters.""" + """Accept all valid characters.""" url = reverse('bot:offtopicchannelname-list', host='api') names = ( '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹', @@ -122,7 +122,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_400_for_missing_name_param(self): - """Test does this return error message when name not provided.""" + """Return error message when name not provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.post(url) self.assertEqual(response.status_code, 400) @@ -131,7 +131,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_bad_name_param(self): - """Test does this return error message when invalid characters provided.""" + """Return error message when invalid characters provided.""" url = reverse('bot:offtopicchannelname-list', host='api') invalid_names = ( 'space between words', @@ -154,21 +154,21 @@ class DeletionTests(APISubdomainTestCase): cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') def test_deleting_unknown_name_returns_404(self): - """Test does this return 404 code when trying to delete unknown name.""" + """Return 404 reponse when trying to delete unknown name.""" url = reverse('bot:offtopicchannelname-detail', args=('unknown-name',), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 404) def test_deleting_known_name_returns_204(self): - """Test does this return 204 code when deleting was successful.""" + """Return 204 response when deleting was successful.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name.name,), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 204) def test_name_gets_deleted(self): - """Test does name gets actually deleted.""" + """Name gets actually deleted.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api') response = self.client.delete(url) -- cgit v1.2.3 From 32342aca3309b4fd482841cb84e4f166152d5c33 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 29 Aug 2020 09:17:15 +0300 Subject: Fix mess with migrations --- .../api/migrations/0051_create_news_setting.py | 25 ---- .../api/migrations/0052_create_news_setting.py | 25 ++++ .../migrations/0052_offtopicchannelname_used.py | 18 --- .../api/migrations/0052_remove_user_avatar_hash.py | 17 --- .../migrations/0053_offtopicchannelname_used.py | 18 +++ .../api/migrations/0053_user_roles_to_array.py | 24 ---- .../api/migrations/0054_remove_user_avatar_hash.py | 17 +++ .../0054_user_invalidate_unknown_role.py | 21 --- .../api/migrations/0055_merge_20200714_2027.py | 14 -- .../apps/api/migrations/0055_reminder_mentions.py | 20 --- .../api/migrations/0055_user_roles_to_array.py | 24 ++++ .../api/migrations/0056_allow_blank_user_roles.py | 21 --- .../0056_user_invalidate_unknown_role.py | 21 +++ .../api/migrations/0057_merge_20200716_0751.py | 14 -- .../apps/api/migrations/0057_reminder_mentions.py | 20 +++ .../api/migrations/0058_allow_blank_user_roles.py | 21 +++ .../migrations/0058_create_new_filterlist_model.py | 33 ----- .../migrations/0059_create_new_filterlist_model.py | 33 +++++ .../api/migrations/0059_populate_filterlists.py | 153 --------------------- .../api/migrations/0060_populate_filterlists.py | 153 +++++++++++++++++++++ .../migrations/0060_populate_filterlists_fix.py | 85 ------------ .../migrations/0061_populate_filterlists_fix.py | 85 ++++++++++++ 22 files changed, 417 insertions(+), 445 deletions(-) delete mode 100644 pydis_site/apps/api/migrations/0051_create_news_setting.py create mode 100644 pydis_site/apps/api/migrations/0052_create_news_setting.py delete mode 100644 pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py delete mode 100644 pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py create mode 100644 pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py delete mode 100644 pydis_site/apps/api/migrations/0053_user_roles_to_array.py create mode 100644 pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py delete mode 100644 pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py delete mode 100644 pydis_site/apps/api/migrations/0055_merge_20200714_2027.py delete mode 100644 pydis_site/apps/api/migrations/0055_reminder_mentions.py create mode 100644 pydis_site/apps/api/migrations/0055_user_roles_to_array.py delete mode 100644 pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py create mode 100644 pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py delete mode 100644 pydis_site/apps/api/migrations/0057_merge_20200716_0751.py create mode 100644 pydis_site/apps/api/migrations/0057_reminder_mentions.py create mode 100644 pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py delete mode 100644 pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py create mode 100644 pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py delete mode 100644 pydis_site/apps/api/migrations/0059_populate_filterlists.py create mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists.py delete mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py create mode 100644 pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py deleted file mode 100644 index f18fdfb1..00000000 --- a/pydis_site/apps/api/migrations/0051_create_news_setting.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import migrations - - -def up(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - setting = BotSetting( - name='news', - data={} - ).save() - - -def down(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - BotSetting.objects.get(name='news').delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0050_remove_infractions_active_default_value'), - ] - - operations = [ - migrations.RunPython(up, down) - ] diff --git a/pydis_site/apps/api/migrations/0052_create_news_setting.py b/pydis_site/apps/api/migrations/0052_create_news_setting.py new file mode 100644 index 00000000..b101d19d --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_create_news_setting.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +def up(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + setting = BotSetting( + name='news', + data={} + ).save() + + +def down(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + BotSetting.objects.get(name='news').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_allow_blank_message_embeds'), + ] + + operations = [ + migrations.RunPython(up, down) + ] diff --git a/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py deleted file mode 100644 index dfdf3835..00000000 --- a/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.11 on 2020-03-30 10:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_create_news_setting'), - ] - - operations = [ - migrations.AddField( - model_name='offtopicchannelname', - name='used', - field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py deleted file mode 100644 index 26b3b954..00000000 --- a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.11 on 2020-05-27 07:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_create_news_setting'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='avatar_hash', - ), - ] diff --git a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py new file mode 100644 index 00000000..b51ce1d2 --- /dev/null +++ b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.11 on 2020-03-30 10:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0052_create_news_setting'), + ] + + operations = [ + migrations.AddField( + model_name='offtopicchannelname', + name='used', + field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), + ), + ] diff --git a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py deleted file mode 100644 index 7ff3a548..00000000 --- a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 13:42 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0052_remove_user_avatar_hash'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='roles', - ), - migrations.AddField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py new file mode 100644 index 00000000..be9fd948 --- /dev/null +++ b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.11 on 2020-05-27 07:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0053_offtopicchannelname_used'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='avatar_hash', + ), + ] diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py deleted file mode 100644 index 96230015..00000000 --- a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 20:08 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0053_user_roles_to_array'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py deleted file mode 100644 index f2a0e638..00000000 --- a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:27 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_allow_blank_message_embeds'), - ('api', '0054_user_invalidate_unknown_role'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py deleted file mode 100644 index d73b450d..00000000 --- a/pydis_site/apps/api/migrations/0055_reminder_mentions.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-15 07:37 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0054_user_invalidate_unknown_role'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='mentions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py new file mode 100644 index 00000000..e7b4a983 --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.11 on 2020-06-02 13:42 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0054_remove_user_avatar_hash'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='roles', + ), + migrations.AddField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py deleted file mode 100644 index 489941c7..00000000 --- a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:35 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_merge_20200714_2027'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py new file mode 100644 index 00000000..ab2696aa --- /dev/null +++ b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.11 on 2020-06-02 20:08 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_user_roles_to_array'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py deleted file mode 100644 index 47a6d2d4..00000000 --- a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-16 07:51 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_reminder_mentions'), - ('api', '0056_allow_blank_user_roles'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0057_reminder_mentions.py b/pydis_site/apps/api/migrations/0057_reminder_mentions.py new file mode 100644 index 00000000..fb829a17 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_reminder_mentions.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.14 on 2020-07-15 07:37 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0056_user_invalidate_unknown_role'), + ] + + operations = [ + migrations.AddField( + model_name='reminder', + name='mentions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py new file mode 100644 index 00000000..8f7fddfc --- /dev/null +++ b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:35 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0057_reminder_mentions'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py deleted file mode 100644 index aecfdad7..00000000 --- a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0057_merge_20200716_0751'), - ] - - operations = [ - migrations.CreateModel( - name='FilterList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField( - choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), - ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], - help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allow or denylist.')), - ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.AddConstraint( - model_name='filterlist', - constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') - ) - ] diff --git a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py new file mode 100644 index 00000000..eac5542d --- /dev/null +++ b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0058_allow_blank_user_roles'), + ] + + operations = [ + migrations.CreateModel( + name='FilterList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField( + choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], + help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='filterlist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') + ) + ] diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py deleted file mode 100644 index 8c550191..00000000 --- a/pydis_site/apps/api/migrations/0059_populate_filterlists.py +++ /dev/null @@ -1,153 +0,0 @@ -from django.db import migrations - -guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -domain_name_blacklist = [ - ("pornhub.com", None, False), - ("liveleak.com", None, False), - ("grabify.link", None, False), - ("bmwforum.co", None, False), - ("leancoding.co", None, False), - ("spottyfly.com", None, False), - ("stopify.co", None, False), - ("yoütu.be", None, False), - ("discörd.com", None, False), - ("minecräft.com", None, False), - ("freegiftcards.co", None, False), - ("disçordapp.com", None, False), - ("fortnight.space", None, False), - ("fortnitechat.site", None, False), - ("joinmy.site", None, False), - ("curiouscat.club", None, False), - ("catsnthings.fun", None, False), - ("yourtube.site", None, False), - ("youtubeshort.watch", None, False), - ("catsnthing.com", None, False), - ("youtubeshort.pro", None, False), - ("canadianlumberjacks.online", None, False), - ("poweredbydialup.club", None, False), - ("poweredbydialup.online", None, False), - ("poweredbysecurity.org", None, False), - ("poweredbysecurity.online", None, False), - ("ssteam.site", None, False), - ("steamwalletgift.com", None, False), - ("discord.gift", None, False), - ("lmgtfy.com", None, False), -] - -filter_token_blacklist = [ - ("\bgoo+ks*\b", None, False), - ("\bky+s+\b", None, False), - ("\bki+ke+s*\b", None, False), - ("\bbeaner+s?\b", None, False), - ("\bcoo+ns*\b", None, False), - ("\bnig+lets*\b", None, False), - ("\bslant-eyes*\b", None, False), - ("\btowe?l-?head+s*\b", None, False), - ("\bchi*n+k+s*\b", None, False), - ("\bspick*s*\b", None, False), - ("\bkill* +(?:yo)?urself+\b", None, False), - ("\bjew+s*\b", None, False), - ("\bsuicide\b", None, False), - ("\brape\b", None, False), - ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), - ("\bta+r+d+\b", None, False), - ("\bcunts*\b", None, False), - ("\btrann*y\b", None, False), - ("\bshemale\b", None, False), - ("fa+g+s*", None, False), - ("卐", None, False), - ("卍", None, False), - ("࿖", None, False), - ("࿕", None, False), - ("࿘", None, False), - ("࿗", None, False), - ("cuck(?!oo+)", None, False), - ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), - ("fag+o+t+s*", None, False), -] - -file_format_whitelist = [ - (".3gp", None, True), - (".3g2", None, True), - (".avi", None, True), - (".bmp", None, True), - (".gif", None, True), - (".h264", None, True), - (".jpg", None, True), - (".jpeg", None, True), - (".m4v", None, True), - (".mkv", None, True), - (".mov", None, True), - (".mp4", None, True), - (".mpeg", None, True), - (".mpg", None, True), - (".png", None, True), - (".tiff", None, True), - (".wmv", None, True), - (".svg", None, True), - (".psd", "Photoshop", True), - (".ai", "Illustrator", True), - (".aep", "After Effects", True), - (".xcf", "GIMP", True), - (".mp3", None, True), - (".wav", None, True), - (".ogg", None, True), - (".webm", None, True), - (".webp", None, True), -] - -populate_data = { - "FILTER_TOKEN": filter_token_blacklist, - "DOMAIN_NAME": domain_name_blacklist, - "FILE_FORMAT": file_format_whitelist, - "GUILD_INVITE": guild_invite_whitelist, -} - - -class Migration(migrations.Migration): - dependencies = [("api", "0058_create_new_filterlist_model")] - - def populate_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - - for filterlist_type, metadata in populate_data.items(): - for content, comment, allowed in metadata: - FilterList.objects.create( - type=filterlist_type, - allowed=allowed, - content=content, - comment=comment, - ) - - def clear_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.all().delete() - - operations = [ - migrations.RunPython(populate_filterlists, clear_filterlists) - ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists.py b/pydis_site/apps/api/migrations/0060_populate_filterlists.py new file mode 100644 index 00000000..35fde95a --- /dev/null +++ b/pydis_site/apps/api/migrations/0060_populate_filterlists.py @@ -0,0 +1,153 @@ +from django.db import migrations + +guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +domain_name_blacklist = [ + ("pornhub.com", None, False), + ("liveleak.com", None, False), + ("grabify.link", None, False), + ("bmwforum.co", None, False), + ("leancoding.co", None, False), + ("spottyfly.com", None, False), + ("stopify.co", None, False), + ("yoütu.be", None, False), + ("discörd.com", None, False), + ("minecräft.com", None, False), + ("freegiftcards.co", None, False), + ("disçordapp.com", None, False), + ("fortnight.space", None, False), + ("fortnitechat.site", None, False), + ("joinmy.site", None, False), + ("curiouscat.club", None, False), + ("catsnthings.fun", None, False), + ("yourtube.site", None, False), + ("youtubeshort.watch", None, False), + ("catsnthing.com", None, False), + ("youtubeshort.pro", None, False), + ("canadianlumberjacks.online", None, False), + ("poweredbydialup.club", None, False), + ("poweredbydialup.online", None, False), + ("poweredbysecurity.org", None, False), + ("poweredbysecurity.online", None, False), + ("ssteam.site", None, False), + ("steamwalletgift.com", None, False), + ("discord.gift", None, False), + ("lmgtfy.com", None, False), +] + +filter_token_blacklist = [ + ("\bgoo+ks*\b", None, False), + ("\bky+s+\b", None, False), + ("\bki+ke+s*\b", None, False), + ("\bbeaner+s?\b", None, False), + ("\bcoo+ns*\b", None, False), + ("\bnig+lets*\b", None, False), + ("\bslant-eyes*\b", None, False), + ("\btowe?l-?head+s*\b", None, False), + ("\bchi*n+k+s*\b", None, False), + ("\bspick*s*\b", None, False), + ("\bkill* +(?:yo)?urself+\b", None, False), + ("\bjew+s*\b", None, False), + ("\bsuicide\b", None, False), + ("\brape\b", None, False), + ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), + ("\bta+r+d+\b", None, False), + ("\bcunts*\b", None, False), + ("\btrann*y\b", None, False), + ("\bshemale\b", None, False), + ("fa+g+s*", None, False), + ("卐", None, False), + ("卍", None, False), + ("࿖", None, False), + ("࿕", None, False), + ("࿘", None, False), + ("࿗", None, False), + ("cuck(?!oo+)", None, False), + ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), + ("fag+o+t+s*", None, False), +] + +file_format_whitelist = [ + (".3gp", None, True), + (".3g2", None, True), + (".avi", None, True), + (".bmp", None, True), + (".gif", None, True), + (".h264", None, True), + (".jpg", None, True), + (".jpeg", None, True), + (".m4v", None, True), + (".mkv", None, True), + (".mov", None, True), + (".mp4", None, True), + (".mpeg", None, True), + (".mpg", None, True), + (".png", None, True), + (".tiff", None, True), + (".wmv", None, True), + (".svg", None, True), + (".psd", "Photoshop", True), + (".ai", "Illustrator", True), + (".aep", "After Effects", True), + (".xcf", "GIMP", True), + (".mp3", None, True), + (".wav", None, True), + (".ogg", None, True), + (".webm", None, True), + (".webp", None, True), +] + +populate_data = { + "FILTER_TOKEN": filter_token_blacklist, + "DOMAIN_NAME": domain_name_blacklist, + "FILE_FORMAT": file_format_whitelist, + "GUILD_INVITE": guild_invite_whitelist, +} + + +class Migration(migrations.Migration): + dependencies = [("api", "0059_create_new_filterlist_model")] + + def populate_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + + for filterlist_type, metadata in populate_data.items(): + for content, comment, allowed in metadata: + FilterList.objects.create( + type=filterlist_type, + allowed=allowed, + content=content, + comment=comment, + ) + + def clear_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.all().delete() + + operations = [ + migrations.RunPython(populate_filterlists, clear_filterlists) + ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py deleted file mode 100644 index 53846f02..00000000 --- a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py +++ /dev/null @@ -1,85 +0,0 @@ -from django.db import migrations - -bad_guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -guild_invite_whitelist = [ - ("267624335836053506", "Python Discord", True), - ("348658686962696195", "RLBot", True), - ("423249981340778496", "Kivy", True), - ("438622377094414346", "Pyglet", True), - ("524691714909274162", "Panda3D", True), - ("666560367173828639", "PyWeek", True), - ("702724176489873509", "Microsoft Python", True), - ("222078108977594368", "Discord.js Official", True), - ("238666723824238602", "Programming Discussions", True), - ("433980600391696384", "JetBrains Community", True), - ("204621105720328193", "Raspberry Pie", True), - ("286633898581164032", "Ren'Py", True), - ("440186186024222721", "Python Discord: Emojis 1", True), - ("578587418123304970", "Python Discord: Emojis 2", True), - ("159039020565790721", "Django", True), - ("273944235143593984", "STEM", True), - ("336642139381301249", "discord.py", True), - ("244230771232079873", "Programmers Hangout", True), - ("239433591950540801", "SpeakJS", True), - ("280033776820813825", "Functional Programming", True), - ("349505959032389632", "PyGame", True), - ("488751051629920277", "Python Atlanta", True), - ("143867839282020352", "C#", True), -] - - -class Migration(migrations.Migration): - dependencies = [("api", "0059_populate_filterlists")] - - def fix_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. - - for content, comment, allowed in guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - def restore_bad_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() - - for content, comment, allowed in bad_guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - operations = [ - migrations.RunPython(fix_filterlist, restore_bad_filterlist) - ] diff --git a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py new file mode 100644 index 00000000..eaaafb38 --- /dev/null +++ b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py @@ -0,0 +1,85 @@ +from django.db import migrations + +bad_guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +guild_invite_whitelist = [ + ("267624335836053506", "Python Discord", True), + ("348658686962696195", "RLBot", True), + ("423249981340778496", "Kivy", True), + ("438622377094414346", "Pyglet", True), + ("524691714909274162", "Panda3D", True), + ("666560367173828639", "PyWeek", True), + ("702724176489873509", "Microsoft Python", True), + ("222078108977594368", "Discord.js Official", True), + ("238666723824238602", "Programming Discussions", True), + ("433980600391696384", "JetBrains Community", True), + ("204621105720328193", "Raspberry Pie", True), + ("286633898581164032", "Ren'Py", True), + ("440186186024222721", "Python Discord: Emojis 1", True), + ("578587418123304970", "Python Discord: Emojis 2", True), + ("159039020565790721", "Django", True), + ("273944235143593984", "STEM", True), + ("336642139381301249", "discord.py", True), + ("244230771232079873", "Programmers Hangout", True), + ("239433591950540801", "SpeakJS", True), + ("280033776820813825", "Functional Programming", True), + ("349505959032389632", "PyGame", True), + ("488751051629920277", "Python Atlanta", True), + ("143867839282020352", "C#", True), +] + + +class Migration(migrations.Migration): + dependencies = [("api", "0060_populate_filterlists")] + + def fix_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. + + for content, comment, allowed in guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + def restore_bad_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() + + for content, comment, allowed in bad_guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + operations = [ + migrations.RunPython(fix_filterlist, restore_bad_filterlist) + ] -- cgit v1.2.3 From ac7745e4ea79fc129447e9b472d3dbd49bf00fd6 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 29 Aug 2020 09:21:15 +0300 Subject: Fix linting issues on off-topic viewset --- pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 29978015..826ad25e 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -1,4 +1,4 @@ -from django.db.models import Case, When, Value +from django.db.models import Case, Value, When from django.db.models.query import QuerySet from django.http.request import HttpRequest from django.shortcuts import get_object_or_404 @@ -114,9 +114,13 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): # When any name is used in our listing then this means we reached end of round # and we need to reset all other names `used` to False if any(offtopic_name.used for offtopic_name in queryset): + # These names that we just got have to be excluded from updating used to False self.get_queryset().update( - used=Case( # These names that we just got have to be excluded from updating to False - When(name__in=(offtopic_name.name for offtopic_name in queryset), then=Value(True)), + used=Case( + When( + name__in=(offtopic_name.name for offtopic_name in queryset), + then=Value(True) + ), default=Value(False) ) ) -- cgit v1.2.3 From dae68c46831ef14e6a0715add1025f9af1b5a73d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Aug 2020 10:00:45 -0700 Subject: Revert pull request #348 Revert commit 330a27926a7f2a9bdae56a5928cd81e55ccb38ed to reverse changes made to 6c08da2f3e48db9c9dd452f2e0505c4a61e46592. Migrations were failing when those changes were deployed. --- .../api/migrations/0051_create_news_setting.py | 25 ++++ .../api/migrations/0052_create_news_setting.py | 25 ---- .../api/migrations/0052_remove_user_avatar_hash.py | 17 +++ .../migrations/0053_offtopicchannelname_used.py | 18 --- .../api/migrations/0053_user_roles_to_array.py | 24 ++++ .../api/migrations/0054_remove_user_avatar_hash.py | 17 --- .../0054_user_invalidate_unknown_role.py | 21 +++ .../api/migrations/0055_merge_20200714_2027.py | 14 ++ .../apps/api/migrations/0055_reminder_mentions.py | 20 +++ .../api/migrations/0055_user_roles_to_array.py | 24 ---- .../api/migrations/0056_allow_blank_user_roles.py | 21 +++ .../0056_user_invalidate_unknown_role.py | 21 --- .../api/migrations/0057_merge_20200716_0751.py | 14 ++ .../apps/api/migrations/0057_reminder_mentions.py | 20 --- .../api/migrations/0058_allow_blank_user_roles.py | 21 --- .../migrations/0058_create_new_filterlist_model.py | 33 +++++ .../migrations/0059_create_new_filterlist_model.py | 33 ----- .../api/migrations/0059_populate_filterlists.py | 153 +++++++++++++++++++++ .../api/migrations/0060_populate_filterlists.py | 153 --------------------- .../migrations/0060_populate_filterlists_fix.py | 85 ++++++++++++ .../migrations/0061_populate_filterlists_fix.py | 85 ------------ .../apps/api/models/bot/off_topic_channel_name.py | 5 - .../apps/api/tests/test_off_topic_channel_names.py | 27 +--- .../api/viewsets/bot/off_topic_channel_name.py | 27 +--- 24 files changed, 431 insertions(+), 472 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0051_create_news_setting.py delete mode 100644 pydis_site/apps/api/migrations/0052_create_news_setting.py create mode 100644 pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py delete mode 100644 pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py create mode 100644 pydis_site/apps/api/migrations/0053_user_roles_to_array.py delete mode 100644 pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py create mode 100644 pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py create mode 100644 pydis_site/apps/api/migrations/0055_merge_20200714_2027.py create mode 100644 pydis_site/apps/api/migrations/0055_reminder_mentions.py delete mode 100644 pydis_site/apps/api/migrations/0055_user_roles_to_array.py create mode 100644 pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py delete mode 100644 pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py create mode 100644 pydis_site/apps/api/migrations/0057_merge_20200716_0751.py delete mode 100644 pydis_site/apps/api/migrations/0057_reminder_mentions.py delete mode 100644 pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py create mode 100644 pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py delete mode 100644 pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py create mode 100644 pydis_site/apps/api/migrations/0059_populate_filterlists.py delete mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists.py create mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py delete mode 100644 pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py new file mode 100644 index 00000000..f18fdfb1 --- /dev/null +++ b/pydis_site/apps/api/migrations/0051_create_news_setting.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +def up(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + setting = BotSetting( + name='news', + data={} + ).save() + + +def down(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + BotSetting.objects.get(name='news').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0050_remove_infractions_active_default_value'), + ] + + operations = [ + migrations.RunPython(up, down) + ] diff --git a/pydis_site/apps/api/migrations/0052_create_news_setting.py b/pydis_site/apps/api/migrations/0052_create_news_setting.py deleted file mode 100644 index b101d19d..00000000 --- a/pydis_site/apps/api/migrations/0052_create_news_setting.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import migrations - - -def up(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - setting = BotSetting( - name='news', - data={} - ).save() - - -def down(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - BotSetting.objects.get(name='news').delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_allow_blank_message_embeds'), - ] - - operations = [ - migrations.RunPython(up, down) - ] diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py new file mode 100644 index 00000000..26b3b954 --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.11 on 2020-05-27 07:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_create_news_setting'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='avatar_hash', + ), + ] diff --git a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py deleted file mode 100644 index b51ce1d2..00000000 --- a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.11 on 2020-03-30 10:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0052_create_news_setting'), - ] - - operations = [ - migrations.AddField( - model_name='offtopicchannelname', - name='used', - field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py new file mode 100644 index 00000000..7ff3a548 --- /dev/null +++ b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.11 on 2020-06-02 13:42 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0052_remove_user_avatar_hash'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='roles', + ), + migrations.AddField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py deleted file mode 100644 index be9fd948..00000000 --- a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.11 on 2020-05-27 07:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0053_offtopicchannelname_used'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='avatar_hash', - ), - ] diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py new file mode 100644 index 00000000..96230015 --- /dev/null +++ b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.11 on 2020-06-02 20:08 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0053_user_roles_to_array'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py new file mode 100644 index 00000000..f2a0e638 --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_allow_blank_message_embeds'), + ('api', '0054_user_invalidate_unknown_role'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py new file mode 100644 index 00000000..d73b450d --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_reminder_mentions.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.14 on 2020-07-15 07:37 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0054_user_invalidate_unknown_role'), + ] + + operations = [ + migrations.AddField( + model_name='reminder', + name='mentions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py deleted file mode 100644 index e7b4a983..00000000 --- a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 13:42 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0054_remove_user_avatar_hash'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='roles', - ), - migrations.AddField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py new file mode 100644 index 00000000..489941c7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:35 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_merge_20200714_2027'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py deleted file mode 100644 index ab2696aa..00000000 --- a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 20:08 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_user_roles_to_array'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py new file mode 100644 index 00000000..47a6d2d4 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.14 on 2020-07-16 07:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_reminder_mentions'), + ('api', '0056_allow_blank_user_roles'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0057_reminder_mentions.py b/pydis_site/apps/api/migrations/0057_reminder_mentions.py deleted file mode 100644 index fb829a17..00000000 --- a/pydis_site/apps/api/migrations/0057_reminder_mentions.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-15 07:37 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0056_user_invalidate_unknown_role'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='mentions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py deleted file mode 100644 index 8f7fddfc..00000000 --- a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:35 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0057_reminder_mentions'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py new file mode 100644 index 00000000..aecfdad7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0057_merge_20200716_0751'), + ] + + operations = [ + migrations.CreateModel( + name='FilterList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField( + choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], + help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='filterlist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') + ) + ] diff --git a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py deleted file mode 100644 index eac5542d..00000000 --- a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0058_allow_blank_user_roles'), - ] - - operations = [ - migrations.CreateModel( - name='FilterList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField( - choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), - ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], - help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allow or denylist.')), - ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.AddConstraint( - model_name='filterlist', - constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') - ) - ] diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py new file mode 100644 index 00000000..8c550191 --- /dev/null +++ b/pydis_site/apps/api/migrations/0059_populate_filterlists.py @@ -0,0 +1,153 @@ +from django.db import migrations + +guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +domain_name_blacklist = [ + ("pornhub.com", None, False), + ("liveleak.com", None, False), + ("grabify.link", None, False), + ("bmwforum.co", None, False), + ("leancoding.co", None, False), + ("spottyfly.com", None, False), + ("stopify.co", None, False), + ("yoütu.be", None, False), + ("discörd.com", None, False), + ("minecräft.com", None, False), + ("freegiftcards.co", None, False), + ("disçordapp.com", None, False), + ("fortnight.space", None, False), + ("fortnitechat.site", None, False), + ("joinmy.site", None, False), + ("curiouscat.club", None, False), + ("catsnthings.fun", None, False), + ("yourtube.site", None, False), + ("youtubeshort.watch", None, False), + ("catsnthing.com", None, False), + ("youtubeshort.pro", None, False), + ("canadianlumberjacks.online", None, False), + ("poweredbydialup.club", None, False), + ("poweredbydialup.online", None, False), + ("poweredbysecurity.org", None, False), + ("poweredbysecurity.online", None, False), + ("ssteam.site", None, False), + ("steamwalletgift.com", None, False), + ("discord.gift", None, False), + ("lmgtfy.com", None, False), +] + +filter_token_blacklist = [ + ("\bgoo+ks*\b", None, False), + ("\bky+s+\b", None, False), + ("\bki+ke+s*\b", None, False), + ("\bbeaner+s?\b", None, False), + ("\bcoo+ns*\b", None, False), + ("\bnig+lets*\b", None, False), + ("\bslant-eyes*\b", None, False), + ("\btowe?l-?head+s*\b", None, False), + ("\bchi*n+k+s*\b", None, False), + ("\bspick*s*\b", None, False), + ("\bkill* +(?:yo)?urself+\b", None, False), + ("\bjew+s*\b", None, False), + ("\bsuicide\b", None, False), + ("\brape\b", None, False), + ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), + ("\bta+r+d+\b", None, False), + ("\bcunts*\b", None, False), + ("\btrann*y\b", None, False), + ("\bshemale\b", None, False), + ("fa+g+s*", None, False), + ("卐", None, False), + ("卍", None, False), + ("࿖", None, False), + ("࿕", None, False), + ("࿘", None, False), + ("࿗", None, False), + ("cuck(?!oo+)", None, False), + ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), + ("fag+o+t+s*", None, False), +] + +file_format_whitelist = [ + (".3gp", None, True), + (".3g2", None, True), + (".avi", None, True), + (".bmp", None, True), + (".gif", None, True), + (".h264", None, True), + (".jpg", None, True), + (".jpeg", None, True), + (".m4v", None, True), + (".mkv", None, True), + (".mov", None, True), + (".mp4", None, True), + (".mpeg", None, True), + (".mpg", None, True), + (".png", None, True), + (".tiff", None, True), + (".wmv", None, True), + (".svg", None, True), + (".psd", "Photoshop", True), + (".ai", "Illustrator", True), + (".aep", "After Effects", True), + (".xcf", "GIMP", True), + (".mp3", None, True), + (".wav", None, True), + (".ogg", None, True), + (".webm", None, True), + (".webp", None, True), +] + +populate_data = { + "FILTER_TOKEN": filter_token_blacklist, + "DOMAIN_NAME": domain_name_blacklist, + "FILE_FORMAT": file_format_whitelist, + "GUILD_INVITE": guild_invite_whitelist, +} + + +class Migration(migrations.Migration): + dependencies = [("api", "0058_create_new_filterlist_model")] + + def populate_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + + for filterlist_type, metadata in populate_data.items(): + for content, comment, allowed in metadata: + FilterList.objects.create( + type=filterlist_type, + allowed=allowed, + content=content, + comment=comment, + ) + + def clear_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.all().delete() + + operations = [ + migrations.RunPython(populate_filterlists, clear_filterlists) + ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists.py b/pydis_site/apps/api/migrations/0060_populate_filterlists.py deleted file mode 100644 index 35fde95a..00000000 --- a/pydis_site/apps/api/migrations/0060_populate_filterlists.py +++ /dev/null @@ -1,153 +0,0 @@ -from django.db import migrations - -guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -domain_name_blacklist = [ - ("pornhub.com", None, False), - ("liveleak.com", None, False), - ("grabify.link", None, False), - ("bmwforum.co", None, False), - ("leancoding.co", None, False), - ("spottyfly.com", None, False), - ("stopify.co", None, False), - ("yoütu.be", None, False), - ("discörd.com", None, False), - ("minecräft.com", None, False), - ("freegiftcards.co", None, False), - ("disçordapp.com", None, False), - ("fortnight.space", None, False), - ("fortnitechat.site", None, False), - ("joinmy.site", None, False), - ("curiouscat.club", None, False), - ("catsnthings.fun", None, False), - ("yourtube.site", None, False), - ("youtubeshort.watch", None, False), - ("catsnthing.com", None, False), - ("youtubeshort.pro", None, False), - ("canadianlumberjacks.online", None, False), - ("poweredbydialup.club", None, False), - ("poweredbydialup.online", None, False), - ("poweredbysecurity.org", None, False), - ("poweredbysecurity.online", None, False), - ("ssteam.site", None, False), - ("steamwalletgift.com", None, False), - ("discord.gift", None, False), - ("lmgtfy.com", None, False), -] - -filter_token_blacklist = [ - ("\bgoo+ks*\b", None, False), - ("\bky+s+\b", None, False), - ("\bki+ke+s*\b", None, False), - ("\bbeaner+s?\b", None, False), - ("\bcoo+ns*\b", None, False), - ("\bnig+lets*\b", None, False), - ("\bslant-eyes*\b", None, False), - ("\btowe?l-?head+s*\b", None, False), - ("\bchi*n+k+s*\b", None, False), - ("\bspick*s*\b", None, False), - ("\bkill* +(?:yo)?urself+\b", None, False), - ("\bjew+s*\b", None, False), - ("\bsuicide\b", None, False), - ("\brape\b", None, False), - ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), - ("\bta+r+d+\b", None, False), - ("\bcunts*\b", None, False), - ("\btrann*y\b", None, False), - ("\bshemale\b", None, False), - ("fa+g+s*", None, False), - ("卐", None, False), - ("卍", None, False), - ("࿖", None, False), - ("࿕", None, False), - ("࿘", None, False), - ("࿗", None, False), - ("cuck(?!oo+)", None, False), - ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), - ("fag+o+t+s*", None, False), -] - -file_format_whitelist = [ - (".3gp", None, True), - (".3g2", None, True), - (".avi", None, True), - (".bmp", None, True), - (".gif", None, True), - (".h264", None, True), - (".jpg", None, True), - (".jpeg", None, True), - (".m4v", None, True), - (".mkv", None, True), - (".mov", None, True), - (".mp4", None, True), - (".mpeg", None, True), - (".mpg", None, True), - (".png", None, True), - (".tiff", None, True), - (".wmv", None, True), - (".svg", None, True), - (".psd", "Photoshop", True), - (".ai", "Illustrator", True), - (".aep", "After Effects", True), - (".xcf", "GIMP", True), - (".mp3", None, True), - (".wav", None, True), - (".ogg", None, True), - (".webm", None, True), - (".webp", None, True), -] - -populate_data = { - "FILTER_TOKEN": filter_token_blacklist, - "DOMAIN_NAME": domain_name_blacklist, - "FILE_FORMAT": file_format_whitelist, - "GUILD_INVITE": guild_invite_whitelist, -} - - -class Migration(migrations.Migration): - dependencies = [("api", "0059_create_new_filterlist_model")] - - def populate_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - - for filterlist_type, metadata in populate_data.items(): - for content, comment, allowed in metadata: - FilterList.objects.create( - type=filterlist_type, - allowed=allowed, - content=content, - comment=comment, - ) - - def clear_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.all().delete() - - operations = [ - migrations.RunPython(populate_filterlists, clear_filterlists) - ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py new file mode 100644 index 00000000..53846f02 --- /dev/null +++ b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py @@ -0,0 +1,85 @@ +from django.db import migrations + +bad_guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +guild_invite_whitelist = [ + ("267624335836053506", "Python Discord", True), + ("348658686962696195", "RLBot", True), + ("423249981340778496", "Kivy", True), + ("438622377094414346", "Pyglet", True), + ("524691714909274162", "Panda3D", True), + ("666560367173828639", "PyWeek", True), + ("702724176489873509", "Microsoft Python", True), + ("222078108977594368", "Discord.js Official", True), + ("238666723824238602", "Programming Discussions", True), + ("433980600391696384", "JetBrains Community", True), + ("204621105720328193", "Raspberry Pie", True), + ("286633898581164032", "Ren'Py", True), + ("440186186024222721", "Python Discord: Emojis 1", True), + ("578587418123304970", "Python Discord: Emojis 2", True), + ("159039020565790721", "Django", True), + ("273944235143593984", "STEM", True), + ("336642139381301249", "discord.py", True), + ("244230771232079873", "Programmers Hangout", True), + ("239433591950540801", "SpeakJS", True), + ("280033776820813825", "Functional Programming", True), + ("349505959032389632", "PyGame", True), + ("488751051629920277", "Python Atlanta", True), + ("143867839282020352", "C#", True), +] + + +class Migration(migrations.Migration): + dependencies = [("api", "0059_populate_filterlists")] + + def fix_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. + + for content, comment, allowed in guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + def restore_bad_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() + + for content, comment, allowed in bad_guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + operations = [ + migrations.RunPython(fix_filterlist, restore_bad_filterlist) + ] diff --git a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py deleted file mode 100644 index eaaafb38..00000000 --- a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py +++ /dev/null @@ -1,85 +0,0 @@ -from django.db import migrations - -bad_guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -guild_invite_whitelist = [ - ("267624335836053506", "Python Discord", True), - ("348658686962696195", "RLBot", True), - ("423249981340778496", "Kivy", True), - ("438622377094414346", "Pyglet", True), - ("524691714909274162", "Panda3D", True), - ("666560367173828639", "PyWeek", True), - ("702724176489873509", "Microsoft Python", True), - ("222078108977594368", "Discord.js Official", True), - ("238666723824238602", "Programming Discussions", True), - ("433980600391696384", "JetBrains Community", True), - ("204621105720328193", "Raspberry Pie", True), - ("286633898581164032", "Ren'Py", True), - ("440186186024222721", "Python Discord: Emojis 1", True), - ("578587418123304970", "Python Discord: Emojis 2", True), - ("159039020565790721", "Django", True), - ("273944235143593984", "STEM", True), - ("336642139381301249", "discord.py", True), - ("244230771232079873", "Programmers Hangout", True), - ("239433591950540801", "SpeakJS", True), - ("280033776820813825", "Functional Programming", True), - ("349505959032389632", "PyGame", True), - ("488751051629920277", "Python Atlanta", True), - ("143867839282020352", "C#", True), -] - - -class Migration(migrations.Migration): - dependencies = [("api", "0060_populate_filterlists")] - - def fix_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. - - for content, comment, allowed in guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - def restore_bad_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() - - for content, comment, allowed in bad_guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - operations = [ - migrations.RunPython(fix_filterlist, restore_bad_filterlist) - ] diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 403c7465..20e77b9f 100644 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -16,11 +16,6 @@ class OffTopicChannelName(ModelReprMixin, models.Model): help_text="The actual channel name that will be used on our Discord server." ) - used = models.BooleanField( - default=False, - help_text="Whether or not this name has already been used during this rotation", - ) - def __str__(self): """Returns the current off-topic name, for display purposes.""" return self.name diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index 3ab8b22d..bd42cd81 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -10,14 +10,12 @@ class UnauthenticatedTests(APISubdomainTestCase): self.client.force_authenticate(user=None) def test_cannot_read_off_topic_channel_name_list(self): - """Return a 401 response when not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_cannot_read_off_topic_channel_name_list_with_random_item_param(self): - """Return a 401 response when `random_items` provided and not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=no') @@ -26,7 +24,6 @@ class UnauthenticatedTests(APISubdomainTestCase): class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_empty_object(self): - """Return empty list when no names in database.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -34,7 +31,6 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_empty_list_with_get_all_param(self): - """Return empty list when no names and `random_items` param provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=5') @@ -42,7 +38,6 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_400_for_bad_random_items_param(self): - """Return error message when passing not integer as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=totally-a-valid-integer') @@ -52,7 +47,6 @@ class EmptyDatabaseTests(APISubdomainTestCase): }) def test_returns_400_for_negative_random_items_param(self): - """Return error message when passing negative int as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=-5') @@ -65,11 +59,10 @@ class EmptyDatabaseTests(APISubdomainTestCase): class ListTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): - cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand', used=False) - cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True) + cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand') + cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') def test_returns_name_in_list(self): - """Return all off-topic channel names.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -83,21 +76,11 @@ class ListTests(APISubdomainTestCase): ) def test_returns_single_item_with_random_items_param_set_to_1(self): - """Return not-used name instead used.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=1') self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json(), [self.test_name.name]) - - def test_running_out_of_names_with_random_parameter(self): - """Reset names `used` parameter to `False` when running out of names.""" - url = reverse('bot:offtopicchannelname-list', host='api') - response = self.client.get(f'{url}?random_items=2') - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name]) class CreationTests(APISubdomainTestCase): @@ -110,7 +93,6 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_201_for_unicode_chars(self): - """Accept all valid characters.""" url = reverse('bot:offtopicchannelname-list', host='api') names = ( '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹', @@ -122,7 +104,6 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_400_for_missing_name_param(self): - """Return error message when name not provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.post(url) self.assertEqual(response.status_code, 400) @@ -131,7 +112,6 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_bad_name_param(self): - """Return error message when invalid characters provided.""" url = reverse('bot:offtopicchannelname-list', host='api') invalid_names = ( 'space between words', @@ -154,21 +134,18 @@ class DeletionTests(APISubdomainTestCase): cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') def test_deleting_unknown_name_returns_404(self): - """Return 404 reponse when trying to delete unknown name.""" url = reverse('bot:offtopicchannelname-detail', args=('unknown-name',), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 404) def test_deleting_known_name_returns_204(self): - """Return 204 response when deleting was successful.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name.name,), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 204) def test_name_gets_deleted(self): - """Name gets actually deleted.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api') response = self.client.delete(url) diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index 826ad25e..d6da2399 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -1,4 +1,3 @@ -from django.db.models import Case, Value, When from django.db.models.query import QuerySet from django.http.request import HttpRequest from django.shortcuts import get_object_or_404 @@ -21,9 +20,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): Return all known off-topic channel names from the database. If the `random_items` query parameter is given, for example using... $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?random_items=5 - ... then the API will return `5` random items from the database - that is not used in current rotation. - When running out of names, API will mark all names to not used and start new rotation. + ... then the API will return `5` random items from the database. #### Response format Return a list of off-topic-channel names: @@ -109,27 +106,7 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) - queryset = self.get_queryset().order_by('used', '?')[:random_count] - - # When any name is used in our listing then this means we reached end of round - # and we need to reset all other names `used` to False - if any(offtopic_name.used for offtopic_name in queryset): - # These names that we just got have to be excluded from updating used to False - self.get_queryset().update( - used=Case( - When( - name__in=(offtopic_name.name for offtopic_name in queryset), - then=Value(True) - ), - default=Value(False) - ) - ) - else: - # Otherwise mark selected names `used` to True - self.get_queryset().filter( - name__in=(offtopic_name.name for offtopic_name in queryset) - ).update(used=True) - + queryset = self.get_queryset().order_by('?')[:random_count] serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From c1ec2f1f9fe443cdde3750b1df1c3d969df3e67e Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 30 Aug 2020 08:20:25 +0300 Subject: Revert "Revert pull request #348" This reverts commit dae68c46831ef14e6a0715add1025f9af1b5a73d. --- .../api/migrations/0051_create_news_setting.py | 25 ---- .../api/migrations/0052_create_news_setting.py | 25 ++++ .../api/migrations/0052_remove_user_avatar_hash.py | 17 --- .../migrations/0053_offtopicchannelname_used.py | 18 +++ .../api/migrations/0053_user_roles_to_array.py | 24 ---- .../api/migrations/0054_remove_user_avatar_hash.py | 17 +++ .../0054_user_invalidate_unknown_role.py | 21 --- .../api/migrations/0055_merge_20200714_2027.py | 14 -- .../apps/api/migrations/0055_reminder_mentions.py | 20 --- .../api/migrations/0055_user_roles_to_array.py | 24 ++++ .../api/migrations/0056_allow_blank_user_roles.py | 21 --- .../0056_user_invalidate_unknown_role.py | 21 +++ .../api/migrations/0057_merge_20200716_0751.py | 14 -- .../apps/api/migrations/0057_reminder_mentions.py | 20 +++ .../api/migrations/0058_allow_blank_user_roles.py | 21 +++ .../migrations/0058_create_new_filterlist_model.py | 33 ----- .../migrations/0059_create_new_filterlist_model.py | 33 +++++ .../api/migrations/0059_populate_filterlists.py | 153 --------------------- .../api/migrations/0060_populate_filterlists.py | 153 +++++++++++++++++++++ .../migrations/0060_populate_filterlists_fix.py | 85 ------------ .../migrations/0061_populate_filterlists_fix.py | 85 ++++++++++++ .../apps/api/models/bot/off_topic_channel_name.py | 5 + .../apps/api/tests/test_off_topic_channel_names.py | 27 +++- .../api/viewsets/bot/off_topic_channel_name.py | 27 +++- 24 files changed, 472 insertions(+), 431 deletions(-) delete mode 100644 pydis_site/apps/api/migrations/0051_create_news_setting.py create mode 100644 pydis_site/apps/api/migrations/0052_create_news_setting.py delete mode 100644 pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py create mode 100644 pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py delete mode 100644 pydis_site/apps/api/migrations/0053_user_roles_to_array.py create mode 100644 pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py delete mode 100644 pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py delete mode 100644 pydis_site/apps/api/migrations/0055_merge_20200714_2027.py delete mode 100644 pydis_site/apps/api/migrations/0055_reminder_mentions.py create mode 100644 pydis_site/apps/api/migrations/0055_user_roles_to_array.py delete mode 100644 pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py create mode 100644 pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py delete mode 100644 pydis_site/apps/api/migrations/0057_merge_20200716_0751.py create mode 100644 pydis_site/apps/api/migrations/0057_reminder_mentions.py create mode 100644 pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py delete mode 100644 pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py create mode 100644 pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py delete mode 100644 pydis_site/apps/api/migrations/0059_populate_filterlists.py create mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists.py delete mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py create mode 100644 pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py deleted file mode 100644 index f18fdfb1..00000000 --- a/pydis_site/apps/api/migrations/0051_create_news_setting.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import migrations - - -def up(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - setting = BotSetting( - name='news', - data={} - ).save() - - -def down(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - BotSetting.objects.get(name='news').delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0050_remove_infractions_active_default_value'), - ] - - operations = [ - migrations.RunPython(up, down) - ] diff --git a/pydis_site/apps/api/migrations/0052_create_news_setting.py b/pydis_site/apps/api/migrations/0052_create_news_setting.py new file mode 100644 index 00000000..b101d19d --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_create_news_setting.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +def up(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + setting = BotSetting( + name='news', + data={} + ).save() + + +def down(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + BotSetting.objects.get(name='news').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_allow_blank_message_embeds'), + ] + + operations = [ + migrations.RunPython(up, down) + ] diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py deleted file mode 100644 index 26b3b954..00000000 --- a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.11 on 2020-05-27 07:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_create_news_setting'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='avatar_hash', - ), - ] diff --git a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py new file mode 100644 index 00000000..b51ce1d2 --- /dev/null +++ b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.11 on 2020-03-30 10:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0052_create_news_setting'), + ] + + operations = [ + migrations.AddField( + model_name='offtopicchannelname', + name='used', + field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), + ), + ] diff --git a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py deleted file mode 100644 index 7ff3a548..00000000 --- a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 13:42 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0052_remove_user_avatar_hash'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='roles', - ), - migrations.AddField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py new file mode 100644 index 00000000..be9fd948 --- /dev/null +++ b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.11 on 2020-05-27 07:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0053_offtopicchannelname_used'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='avatar_hash', + ), + ] diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py deleted file mode 100644 index 96230015..00000000 --- a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 20:08 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0053_user_roles_to_array'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py deleted file mode 100644 index f2a0e638..00000000 --- a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:27 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_allow_blank_message_embeds'), - ('api', '0054_user_invalidate_unknown_role'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py deleted file mode 100644 index d73b450d..00000000 --- a/pydis_site/apps/api/migrations/0055_reminder_mentions.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-15 07:37 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0054_user_invalidate_unknown_role'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='mentions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py new file mode 100644 index 00000000..e7b4a983 --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.11 on 2020-06-02 13:42 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0054_remove_user_avatar_hash'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='roles', + ), + migrations.AddField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py deleted file mode 100644 index 489941c7..00000000 --- a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:35 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_merge_20200714_2027'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py new file mode 100644 index 00000000..ab2696aa --- /dev/null +++ b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.11 on 2020-06-02 20:08 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_user_roles_to_array'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py deleted file mode 100644 index 47a6d2d4..00000000 --- a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-16 07:51 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_reminder_mentions'), - ('api', '0056_allow_blank_user_roles'), - ] - - operations = [ - ] diff --git a/pydis_site/apps/api/migrations/0057_reminder_mentions.py b/pydis_site/apps/api/migrations/0057_reminder_mentions.py new file mode 100644 index 00000000..fb829a17 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_reminder_mentions.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.14 on 2020-07-15 07:37 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0056_user_invalidate_unknown_role'), + ] + + operations = [ + migrations.AddField( + model_name='reminder', + name='mentions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py new file mode 100644 index 00000000..8f7fddfc --- /dev/null +++ b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:35 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0057_reminder_mentions'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py deleted file mode 100644 index aecfdad7..00000000 --- a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0057_merge_20200716_0751'), - ] - - operations = [ - migrations.CreateModel( - name='FilterList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField( - choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), - ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], - help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allow or denylist.')), - ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.AddConstraint( - model_name='filterlist', - constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') - ) - ] diff --git a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py new file mode 100644 index 00000000..eac5542d --- /dev/null +++ b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0058_allow_blank_user_roles'), + ] + + operations = [ + migrations.CreateModel( + name='FilterList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField( + choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], + help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='filterlist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') + ) + ] diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py deleted file mode 100644 index 8c550191..00000000 --- a/pydis_site/apps/api/migrations/0059_populate_filterlists.py +++ /dev/null @@ -1,153 +0,0 @@ -from django.db import migrations - -guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -domain_name_blacklist = [ - ("pornhub.com", None, False), - ("liveleak.com", None, False), - ("grabify.link", None, False), - ("bmwforum.co", None, False), - ("leancoding.co", None, False), - ("spottyfly.com", None, False), - ("stopify.co", None, False), - ("yoütu.be", None, False), - ("discörd.com", None, False), - ("minecräft.com", None, False), - ("freegiftcards.co", None, False), - ("disçordapp.com", None, False), - ("fortnight.space", None, False), - ("fortnitechat.site", None, False), - ("joinmy.site", None, False), - ("curiouscat.club", None, False), - ("catsnthings.fun", None, False), - ("yourtube.site", None, False), - ("youtubeshort.watch", None, False), - ("catsnthing.com", None, False), - ("youtubeshort.pro", None, False), - ("canadianlumberjacks.online", None, False), - ("poweredbydialup.club", None, False), - ("poweredbydialup.online", None, False), - ("poweredbysecurity.org", None, False), - ("poweredbysecurity.online", None, False), - ("ssteam.site", None, False), - ("steamwalletgift.com", None, False), - ("discord.gift", None, False), - ("lmgtfy.com", None, False), -] - -filter_token_blacklist = [ - ("\bgoo+ks*\b", None, False), - ("\bky+s+\b", None, False), - ("\bki+ke+s*\b", None, False), - ("\bbeaner+s?\b", None, False), - ("\bcoo+ns*\b", None, False), - ("\bnig+lets*\b", None, False), - ("\bslant-eyes*\b", None, False), - ("\btowe?l-?head+s*\b", None, False), - ("\bchi*n+k+s*\b", None, False), - ("\bspick*s*\b", None, False), - ("\bkill* +(?:yo)?urself+\b", None, False), - ("\bjew+s*\b", None, False), - ("\bsuicide\b", None, False), - ("\brape\b", None, False), - ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), - ("\bta+r+d+\b", None, False), - ("\bcunts*\b", None, False), - ("\btrann*y\b", None, False), - ("\bshemale\b", None, False), - ("fa+g+s*", None, False), - ("卐", None, False), - ("卍", None, False), - ("࿖", None, False), - ("࿕", None, False), - ("࿘", None, False), - ("࿗", None, False), - ("cuck(?!oo+)", None, False), - ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), - ("fag+o+t+s*", None, False), -] - -file_format_whitelist = [ - (".3gp", None, True), - (".3g2", None, True), - (".avi", None, True), - (".bmp", None, True), - (".gif", None, True), - (".h264", None, True), - (".jpg", None, True), - (".jpeg", None, True), - (".m4v", None, True), - (".mkv", None, True), - (".mov", None, True), - (".mp4", None, True), - (".mpeg", None, True), - (".mpg", None, True), - (".png", None, True), - (".tiff", None, True), - (".wmv", None, True), - (".svg", None, True), - (".psd", "Photoshop", True), - (".ai", "Illustrator", True), - (".aep", "After Effects", True), - (".xcf", "GIMP", True), - (".mp3", None, True), - (".wav", None, True), - (".ogg", None, True), - (".webm", None, True), - (".webp", None, True), -] - -populate_data = { - "FILTER_TOKEN": filter_token_blacklist, - "DOMAIN_NAME": domain_name_blacklist, - "FILE_FORMAT": file_format_whitelist, - "GUILD_INVITE": guild_invite_whitelist, -} - - -class Migration(migrations.Migration): - dependencies = [("api", "0058_create_new_filterlist_model")] - - def populate_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - - for filterlist_type, metadata in populate_data.items(): - for content, comment, allowed in metadata: - FilterList.objects.create( - type=filterlist_type, - allowed=allowed, - content=content, - comment=comment, - ) - - def clear_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.all().delete() - - operations = [ - migrations.RunPython(populate_filterlists, clear_filterlists) - ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists.py b/pydis_site/apps/api/migrations/0060_populate_filterlists.py new file mode 100644 index 00000000..35fde95a --- /dev/null +++ b/pydis_site/apps/api/migrations/0060_populate_filterlists.py @@ -0,0 +1,153 @@ +from django.db import migrations + +guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +domain_name_blacklist = [ + ("pornhub.com", None, False), + ("liveleak.com", None, False), + ("grabify.link", None, False), + ("bmwforum.co", None, False), + ("leancoding.co", None, False), + ("spottyfly.com", None, False), + ("stopify.co", None, False), + ("yoütu.be", None, False), + ("discörd.com", None, False), + ("minecräft.com", None, False), + ("freegiftcards.co", None, False), + ("disçordapp.com", None, False), + ("fortnight.space", None, False), + ("fortnitechat.site", None, False), + ("joinmy.site", None, False), + ("curiouscat.club", None, False), + ("catsnthings.fun", None, False), + ("yourtube.site", None, False), + ("youtubeshort.watch", None, False), + ("catsnthing.com", None, False), + ("youtubeshort.pro", None, False), + ("canadianlumberjacks.online", None, False), + ("poweredbydialup.club", None, False), + ("poweredbydialup.online", None, False), + ("poweredbysecurity.org", None, False), + ("poweredbysecurity.online", None, False), + ("ssteam.site", None, False), + ("steamwalletgift.com", None, False), + ("discord.gift", None, False), + ("lmgtfy.com", None, False), +] + +filter_token_blacklist = [ + ("\bgoo+ks*\b", None, False), + ("\bky+s+\b", None, False), + ("\bki+ke+s*\b", None, False), + ("\bbeaner+s?\b", None, False), + ("\bcoo+ns*\b", None, False), + ("\bnig+lets*\b", None, False), + ("\bslant-eyes*\b", None, False), + ("\btowe?l-?head+s*\b", None, False), + ("\bchi*n+k+s*\b", None, False), + ("\bspick*s*\b", None, False), + ("\bkill* +(?:yo)?urself+\b", None, False), + ("\bjew+s*\b", None, False), + ("\bsuicide\b", None, False), + ("\brape\b", None, False), + ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), + ("\bta+r+d+\b", None, False), + ("\bcunts*\b", None, False), + ("\btrann*y\b", None, False), + ("\bshemale\b", None, False), + ("fa+g+s*", None, False), + ("卐", None, False), + ("卍", None, False), + ("࿖", None, False), + ("࿕", None, False), + ("࿘", None, False), + ("࿗", None, False), + ("cuck(?!oo+)", None, False), + ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), + ("fag+o+t+s*", None, False), +] + +file_format_whitelist = [ + (".3gp", None, True), + (".3g2", None, True), + (".avi", None, True), + (".bmp", None, True), + (".gif", None, True), + (".h264", None, True), + (".jpg", None, True), + (".jpeg", None, True), + (".m4v", None, True), + (".mkv", None, True), + (".mov", None, True), + (".mp4", None, True), + (".mpeg", None, True), + (".mpg", None, True), + (".png", None, True), + (".tiff", None, True), + (".wmv", None, True), + (".svg", None, True), + (".psd", "Photoshop", True), + (".ai", "Illustrator", True), + (".aep", "After Effects", True), + (".xcf", "GIMP", True), + (".mp3", None, True), + (".wav", None, True), + (".ogg", None, True), + (".webm", None, True), + (".webp", None, True), +] + +populate_data = { + "FILTER_TOKEN": filter_token_blacklist, + "DOMAIN_NAME": domain_name_blacklist, + "FILE_FORMAT": file_format_whitelist, + "GUILD_INVITE": guild_invite_whitelist, +} + + +class Migration(migrations.Migration): + dependencies = [("api", "0059_create_new_filterlist_model")] + + def populate_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + + for filterlist_type, metadata in populate_data.items(): + for content, comment, allowed in metadata: + FilterList.objects.create( + type=filterlist_type, + allowed=allowed, + content=content, + comment=comment, + ) + + def clear_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.all().delete() + + operations = [ + migrations.RunPython(populate_filterlists, clear_filterlists) + ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py deleted file mode 100644 index 53846f02..00000000 --- a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py +++ /dev/null @@ -1,85 +0,0 @@ -from django.db import migrations - -bad_guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -guild_invite_whitelist = [ - ("267624335836053506", "Python Discord", True), - ("348658686962696195", "RLBot", True), - ("423249981340778496", "Kivy", True), - ("438622377094414346", "Pyglet", True), - ("524691714909274162", "Panda3D", True), - ("666560367173828639", "PyWeek", True), - ("702724176489873509", "Microsoft Python", True), - ("222078108977594368", "Discord.js Official", True), - ("238666723824238602", "Programming Discussions", True), - ("433980600391696384", "JetBrains Community", True), - ("204621105720328193", "Raspberry Pie", True), - ("286633898581164032", "Ren'Py", True), - ("440186186024222721", "Python Discord: Emojis 1", True), - ("578587418123304970", "Python Discord: Emojis 2", True), - ("159039020565790721", "Django", True), - ("273944235143593984", "STEM", True), - ("336642139381301249", "discord.py", True), - ("244230771232079873", "Programmers Hangout", True), - ("239433591950540801", "SpeakJS", True), - ("280033776820813825", "Functional Programming", True), - ("349505959032389632", "PyGame", True), - ("488751051629920277", "Python Atlanta", True), - ("143867839282020352", "C#", True), -] - - -class Migration(migrations.Migration): - dependencies = [("api", "0059_populate_filterlists")] - - def fix_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. - - for content, comment, allowed in guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - def restore_bad_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() - - for content, comment, allowed in bad_guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - operations = [ - migrations.RunPython(fix_filterlist, restore_bad_filterlist) - ] diff --git a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py new file mode 100644 index 00000000..eaaafb38 --- /dev/null +++ b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py @@ -0,0 +1,85 @@ +from django.db import migrations + +bad_guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +guild_invite_whitelist = [ + ("267624335836053506", "Python Discord", True), + ("348658686962696195", "RLBot", True), + ("423249981340778496", "Kivy", True), + ("438622377094414346", "Pyglet", True), + ("524691714909274162", "Panda3D", True), + ("666560367173828639", "PyWeek", True), + ("702724176489873509", "Microsoft Python", True), + ("222078108977594368", "Discord.js Official", True), + ("238666723824238602", "Programming Discussions", True), + ("433980600391696384", "JetBrains Community", True), + ("204621105720328193", "Raspberry Pie", True), + ("286633898581164032", "Ren'Py", True), + ("440186186024222721", "Python Discord: Emojis 1", True), + ("578587418123304970", "Python Discord: Emojis 2", True), + ("159039020565790721", "Django", True), + ("273944235143593984", "STEM", True), + ("336642139381301249", "discord.py", True), + ("244230771232079873", "Programmers Hangout", True), + ("239433591950540801", "SpeakJS", True), + ("280033776820813825", "Functional Programming", True), + ("349505959032389632", "PyGame", True), + ("488751051629920277", "Python Atlanta", True), + ("143867839282020352", "C#", True), +] + + +class Migration(migrations.Migration): + dependencies = [("api", "0060_populate_filterlists")] + + def fix_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. + + for content, comment, allowed in guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + def restore_bad_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() + + for content, comment, allowed in bad_guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + operations = [ + migrations.RunPython(fix_filterlist, restore_bad_filterlist) + ] diff --git a/pydis_site/apps/api/models/bot/off_topic_channel_name.py b/pydis_site/apps/api/models/bot/off_topic_channel_name.py index 20e77b9f..403c7465 100644 --- a/pydis_site/apps/api/models/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/models/bot/off_topic_channel_name.py @@ -16,6 +16,11 @@ class OffTopicChannelName(ModelReprMixin, models.Model): help_text="The actual channel name that will be used on our Discord server." ) + used = models.BooleanField( + default=False, + help_text="Whether or not this name has already been used during this rotation", + ) + def __str__(self): """Returns the current off-topic name, for display purposes.""" return self.name diff --git a/pydis_site/apps/api/tests/test_off_topic_channel_names.py b/pydis_site/apps/api/tests/test_off_topic_channel_names.py index bd42cd81..3ab8b22d 100644 --- a/pydis_site/apps/api/tests/test_off_topic_channel_names.py +++ b/pydis_site/apps/api/tests/test_off_topic_channel_names.py @@ -10,12 +10,14 @@ class UnauthenticatedTests(APISubdomainTestCase): self.client.force_authenticate(user=None) def test_cannot_read_off_topic_channel_name_list(self): + """Return a 401 response when not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) self.assertEqual(response.status_code, 401) def test_cannot_read_off_topic_channel_name_list_with_random_item_param(self): + """Return a 401 response when `random_items` provided and not authenticated.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=no') @@ -24,6 +26,7 @@ class UnauthenticatedTests(APISubdomainTestCase): class EmptyDatabaseTests(APISubdomainTestCase): def test_returns_empty_object(self): + """Return empty list when no names in database.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -31,6 +34,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_empty_list_with_get_all_param(self): + """Return empty list when no names and `random_items` param provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=5') @@ -38,6 +42,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): self.assertEqual(response.json(), []) def test_returns_400_for_bad_random_items_param(self): + """Return error message when passing not integer as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=totally-a-valid-integer') @@ -47,6 +52,7 @@ class EmptyDatabaseTests(APISubdomainTestCase): }) def test_returns_400_for_negative_random_items_param(self): + """Return error message when passing negative int as `random_items`.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=-5') @@ -59,10 +65,11 @@ class EmptyDatabaseTests(APISubdomainTestCase): class ListTests(APISubdomainTestCase): @classmethod def setUpTestData(cls): - cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand') - cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') + cls.test_name = OffTopicChannelName.objects.create(name='lemons-lemonade-stand', used=False) + cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk', used=True) def test_returns_name_in_list(self): + """Return all off-topic channel names.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(url) @@ -76,11 +83,21 @@ class ListTests(APISubdomainTestCase): ) def test_returns_single_item_with_random_items_param_set_to_1(self): + """Return not-used name instead used.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.get(f'{url}?random_items=1') self.assertEqual(response.status_code, 200) self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json(), [self.test_name.name]) + + def test_running_out_of_names_with_random_parameter(self): + """Reset names `used` parameter to `False` when running out of names.""" + url = reverse('bot:offtopicchannelname-list', host='api') + response = self.client.get(f'{url}?random_items=2') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), [self.test_name.name, self.test_name_2.name]) class CreationTests(APISubdomainTestCase): @@ -93,6 +110,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_201_for_unicode_chars(self): + """Accept all valid characters.""" url = reverse('bot:offtopicchannelname-list', host='api') names = ( '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹', @@ -104,6 +122,7 @@ class CreationTests(APISubdomainTestCase): self.assertEqual(response.status_code, 201) def test_returns_400_for_missing_name_param(self): + """Return error message when name not provided.""" url = reverse('bot:offtopicchannelname-list', host='api') response = self.client.post(url) self.assertEqual(response.status_code, 400) @@ -112,6 +131,7 @@ class CreationTests(APISubdomainTestCase): }) def test_returns_400_for_bad_name_param(self): + """Return error message when invalid characters provided.""" url = reverse('bot:offtopicchannelname-list', host='api') invalid_names = ( 'space between words', @@ -134,18 +154,21 @@ class DeletionTests(APISubdomainTestCase): cls.test_name_2 = OffTopicChannelName.objects.create(name='bbq-with-bisk') def test_deleting_unknown_name_returns_404(self): + """Return 404 reponse when trying to delete unknown name.""" url = reverse('bot:offtopicchannelname-detail', args=('unknown-name',), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 404) def test_deleting_known_name_returns_204(self): + """Return 204 response when deleting was successful.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name.name,), host='api') response = self.client.delete(url) self.assertEqual(response.status_code, 204) def test_name_gets_deleted(self): + """Name gets actually deleted.""" url = reverse('bot:offtopicchannelname-detail', args=(self.test_name_2.name,), host='api') response = self.client.delete(url) diff --git a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py index d6da2399..826ad25e 100644 --- a/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py +++ b/pydis_site/apps/api/viewsets/bot/off_topic_channel_name.py @@ -1,3 +1,4 @@ +from django.db.models import Case, Value, When from django.db.models.query import QuerySet from django.http.request import HttpRequest from django.shortcuts import get_object_or_404 @@ -20,7 +21,9 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): Return all known off-topic channel names from the database. If the `random_items` query parameter is given, for example using... $ curl api.pythondiscord.local:8000/bot/off-topic-channel-names?random_items=5 - ... then the API will return `5` random items from the database. + ... then the API will return `5` random items from the database + that is not used in current rotation. + When running out of names, API will mark all names to not used and start new rotation. #### Response format Return a list of off-topic-channel names: @@ -106,7 +109,27 @@ class OffTopicChannelNameViewSet(DestroyModelMixin, ViewSet): 'random_items': ["Must be a positive integer."] }) - queryset = self.get_queryset().order_by('?')[:random_count] + queryset = self.get_queryset().order_by('used', '?')[:random_count] + + # When any name is used in our listing then this means we reached end of round + # and we need to reset all other names `used` to False + if any(offtopic_name.used for offtopic_name in queryset): + # These names that we just got have to be excluded from updating used to False + self.get_queryset().update( + used=Case( + When( + name__in=(offtopic_name.name for offtopic_name in queryset), + then=Value(True) + ), + default=Value(False) + ) + ) + else: + # Otherwise mark selected names `used` to True + self.get_queryset().filter( + name__in=(offtopic_name.name for offtopic_name in queryset) + ).update(used=True) + serialized = self.serializer_class(queryset, many=True) return Response(serialized.data) -- cgit v1.2.3 From 0ec3148228d7c13a35626059cffdad9dbb0670c2 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 30 Aug 2020 08:21:21 +0300 Subject: Revert "Fix mess with migrations" This reverts commit 32342aca3309b4fd482841cb84e4f166152d5c33. --- .../api/migrations/0051_create_news_setting.py | 25 ++++ .../api/migrations/0052_create_news_setting.py | 25 ---- .../migrations/0052_offtopicchannelname_used.py | 18 +++ .../api/migrations/0052_remove_user_avatar_hash.py | 17 +++ .../migrations/0053_offtopicchannelname_used.py | 18 --- .../api/migrations/0053_user_roles_to_array.py | 24 ++++ .../api/migrations/0054_remove_user_avatar_hash.py | 17 --- .../0054_user_invalidate_unknown_role.py | 21 +++ .../api/migrations/0055_merge_20200714_2027.py | 14 ++ .../apps/api/migrations/0055_reminder_mentions.py | 20 +++ .../api/migrations/0055_user_roles_to_array.py | 24 ---- .../api/migrations/0056_allow_blank_user_roles.py | 21 +++ .../0056_user_invalidate_unknown_role.py | 21 --- .../api/migrations/0057_merge_20200716_0751.py | 14 ++ .../apps/api/migrations/0057_reminder_mentions.py | 20 --- .../api/migrations/0058_allow_blank_user_roles.py | 21 --- .../migrations/0058_create_new_filterlist_model.py | 33 +++++ .../migrations/0059_create_new_filterlist_model.py | 33 ----- .../api/migrations/0059_populate_filterlists.py | 153 +++++++++++++++++++++ .../api/migrations/0060_populate_filterlists.py | 153 --------------------- .../migrations/0060_populate_filterlists_fix.py | 85 ++++++++++++ .../migrations/0061_populate_filterlists_fix.py | 85 ------------ 22 files changed, 445 insertions(+), 417 deletions(-) create mode 100644 pydis_site/apps/api/migrations/0051_create_news_setting.py delete mode 100644 pydis_site/apps/api/migrations/0052_create_news_setting.py create mode 100644 pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py create mode 100644 pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py delete mode 100644 pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py create mode 100644 pydis_site/apps/api/migrations/0053_user_roles_to_array.py delete mode 100644 pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py create mode 100644 pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py create mode 100644 pydis_site/apps/api/migrations/0055_merge_20200714_2027.py create mode 100644 pydis_site/apps/api/migrations/0055_reminder_mentions.py delete mode 100644 pydis_site/apps/api/migrations/0055_user_roles_to_array.py create mode 100644 pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py delete mode 100644 pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py create mode 100644 pydis_site/apps/api/migrations/0057_merge_20200716_0751.py delete mode 100644 pydis_site/apps/api/migrations/0057_reminder_mentions.py delete mode 100644 pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py create mode 100644 pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py delete mode 100644 pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py create mode 100644 pydis_site/apps/api/migrations/0059_populate_filterlists.py delete mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists.py create mode 100644 pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py delete mode 100644 pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0051_create_news_setting.py b/pydis_site/apps/api/migrations/0051_create_news_setting.py new file mode 100644 index 00000000..f18fdfb1 --- /dev/null +++ b/pydis_site/apps/api/migrations/0051_create_news_setting.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +def up(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + setting = BotSetting( + name='news', + data={} + ).save() + + +def down(apps, schema_editor): + BotSetting = apps.get_model('api', 'BotSetting') + BotSetting.objects.get(name='news').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0050_remove_infractions_active_default_value'), + ] + + operations = [ + migrations.RunPython(up, down) + ] diff --git a/pydis_site/apps/api/migrations/0052_create_news_setting.py b/pydis_site/apps/api/migrations/0052_create_news_setting.py deleted file mode 100644 index b101d19d..00000000 --- a/pydis_site/apps/api/migrations/0052_create_news_setting.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import migrations - - -def up(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - setting = BotSetting( - name='news', - data={} - ).save() - - -def down(apps, schema_editor): - BotSetting = apps.get_model('api', 'BotSetting') - BotSetting.objects.get(name='news').delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0051_allow_blank_message_embeds'), - ] - - operations = [ - migrations.RunPython(up, down) - ] diff --git a/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py new file mode 100644 index 00000000..dfdf3835 --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_offtopicchannelname_used.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.11 on 2020-03-30 10:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_create_news_setting'), + ] + + operations = [ + migrations.AddField( + model_name='offtopicchannelname', + name='used', + field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), + ), + ] diff --git a/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py new file mode 100644 index 00000000..26b3b954 --- /dev/null +++ b/pydis_site/apps/api/migrations/0052_remove_user_avatar_hash.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.11 on 2020-05-27 07:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_create_news_setting'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='avatar_hash', + ), + ] diff --git a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py b/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py deleted file mode 100644 index b51ce1d2..00000000 --- a/pydis_site/apps/api/migrations/0053_offtopicchannelname_used.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.11 on 2020-03-30 10:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0052_create_news_setting'), - ] - - operations = [ - migrations.AddField( - model_name='offtopicchannelname', - name='used', - field=models.BooleanField(default=False, help_text='Whether or not this name has already been used during this rotation'), - ), - ] diff --git a/pydis_site/apps/api/migrations/0053_user_roles_to_array.py b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py new file mode 100644 index 00000000..7ff3a548 --- /dev/null +++ b/pydis_site/apps/api/migrations/0053_user_roles_to_array.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.11 on 2020-06-02 13:42 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0052_remove_user_avatar_hash'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='roles', + ), + migrations.AddField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py b/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py deleted file mode 100644 index be9fd948..00000000 --- a/pydis_site/apps/api/migrations/0054_remove_user_avatar_hash.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.11 on 2020-05-27 07:17 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0053_offtopicchannelname_used'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='avatar_hash', - ), - ] diff --git a/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py new file mode 100644 index 00000000..96230015 --- /dev/null +++ b/pydis_site/apps/api/migrations/0054_user_invalidate_unknown_role.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.11 on 2020-06-02 20:08 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0053_user_roles_to_array'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py new file mode 100644 index 00000000..f2a0e638 --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_merge_20200714_2027.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_allow_blank_message_embeds'), + ('api', '0054_user_invalidate_unknown_role'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0055_reminder_mentions.py b/pydis_site/apps/api/migrations/0055_reminder_mentions.py new file mode 100644 index 00000000..d73b450d --- /dev/null +++ b/pydis_site/apps/api/migrations/0055_reminder_mentions.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.14 on 2020-07-15 07:37 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0054_user_invalidate_unknown_role'), + ] + + operations = [ + migrations.AddField( + model_name='reminder', + name='mentions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py b/pydis_site/apps/api/migrations/0055_user_roles_to_array.py deleted file mode 100644 index e7b4a983..00000000 --- a/pydis_site/apps/api/migrations/0055_user_roles_to_array.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 13:42 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0054_remove_user_avatar_hash'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='roles', - ), - migrations.AddField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.')]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py new file mode 100644 index 00000000..489941c7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0056_allow_blank_user_roles.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.8 on 2020-07-14 20:35 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models +import pydis_site.apps.api.models.bot.user + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_merge_20200714_2027'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='roles', + field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), + ), + ] diff --git a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py b/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py deleted file mode 100644 index ab2696aa..00000000 --- a/pydis_site/apps/api/migrations/0056_user_invalidate_unknown_role.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.11 on 2020-06-02 20:08 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0055_user_roles_to_array'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py new file mode 100644 index 00000000..47a6d2d4 --- /dev/null +++ b/pydis_site/apps/api/migrations/0057_merge_20200716_0751.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.14 on 2020-07-16 07:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_reminder_mentions'), + ('api', '0056_allow_blank_user_roles'), + ] + + operations = [ + ] diff --git a/pydis_site/apps/api/migrations/0057_reminder_mentions.py b/pydis_site/apps/api/migrations/0057_reminder_mentions.py deleted file mode 100644 index fb829a17..00000000 --- a/pydis_site/apps/api/migrations/0057_reminder_mentions.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-15 07:37 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0056_user_invalidate_unknown_role'), - ] - - operations = [ - migrations.AddField( - model_name='reminder', - name='mentions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Mention IDs cannot be negative.')]), blank=True, default=list, help_text='IDs of roles or users to ping with the reminder.', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py b/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py deleted file mode 100644 index 8f7fddfc..00000000 --- a/pydis_site/apps/api/migrations/0058_allow_blank_user_roles.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-14 20:35 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import pydis_site.apps.api.models.bot.user - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0057_reminder_mentions'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='roles', - field=django.contrib.postgres.fields.ArrayField(base_field=models.BigIntegerField(validators=[django.core.validators.MinValueValidator(limit_value=0, message='Role IDs cannot be negative.'), pydis_site.apps.api.models.bot.user._validate_existing_role]), blank=True, default=list, help_text='IDs of roles the user has on the server', size=None), - ), - ] diff --git a/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py new file mode 100644 index 00000000..aecfdad7 --- /dev/null +++ b/pydis_site/apps/api/migrations/0058_create_new_filterlist_model.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.8 on 2020-07-15 11:23 + +from django.db import migrations, models +import pydis_site.apps.api.models.mixins + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0057_merge_20200716_0751'), + ] + + operations = [ + migrations.CreateModel( + name='FilterList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField( + choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), + ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], + help_text='The type of allowlist this is on.', max_length=50)), + ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), + ('content', models.TextField(help_text='The data to add to the allow or denylist.')), + ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), + ], + bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), + ), + migrations.AddConstraint( + model_name='filterlist', + constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') + ) + ] diff --git a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py b/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py deleted file mode 100644 index eac5542d..00000000 --- a/pydis_site/apps/api/migrations/0059_create_new_filterlist_model.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.0.8 on 2020-07-15 11:23 - -from django.db import migrations, models -import pydis_site.apps.api.models.mixins - - -class Migration(migrations.Migration): - dependencies = [ - ('api', '0058_allow_blank_user_roles'), - ] - - operations = [ - migrations.CreateModel( - name='FilterList', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('type', models.CharField( - choices=[('GUILD_INVITE', 'Guild Invite'), ('FILE_FORMAT', 'File Format'), - ('DOMAIN_NAME', 'Domain Name'), ('FILTER_TOKEN', 'Filter Token')], - help_text='The type of allowlist this is on.', max_length=50)), - ('allowed', models.BooleanField(help_text='Whether this item is on the allowlist or the denylist.')), - ('content', models.TextField(help_text='The data to add to the allow or denylist.')), - ('comment', models.TextField(help_text="Optional comment on this entry.", null=True)), - ], - bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model), - ), - migrations.AddConstraint( - model_name='filterlist', - constraint=models.UniqueConstraint(fields=('content', 'type'), name='unique_filter_list') - ) - ] diff --git a/pydis_site/apps/api/migrations/0059_populate_filterlists.py b/pydis_site/apps/api/migrations/0059_populate_filterlists.py new file mode 100644 index 00000000..8c550191 --- /dev/null +++ b/pydis_site/apps/api/migrations/0059_populate_filterlists.py @@ -0,0 +1,153 @@ +from django.db import migrations + +guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +domain_name_blacklist = [ + ("pornhub.com", None, False), + ("liveleak.com", None, False), + ("grabify.link", None, False), + ("bmwforum.co", None, False), + ("leancoding.co", None, False), + ("spottyfly.com", None, False), + ("stopify.co", None, False), + ("yoütu.be", None, False), + ("discörd.com", None, False), + ("minecräft.com", None, False), + ("freegiftcards.co", None, False), + ("disçordapp.com", None, False), + ("fortnight.space", None, False), + ("fortnitechat.site", None, False), + ("joinmy.site", None, False), + ("curiouscat.club", None, False), + ("catsnthings.fun", None, False), + ("yourtube.site", None, False), + ("youtubeshort.watch", None, False), + ("catsnthing.com", None, False), + ("youtubeshort.pro", None, False), + ("canadianlumberjacks.online", None, False), + ("poweredbydialup.club", None, False), + ("poweredbydialup.online", None, False), + ("poweredbysecurity.org", None, False), + ("poweredbysecurity.online", None, False), + ("ssteam.site", None, False), + ("steamwalletgift.com", None, False), + ("discord.gift", None, False), + ("lmgtfy.com", None, False), +] + +filter_token_blacklist = [ + ("\bgoo+ks*\b", None, False), + ("\bky+s+\b", None, False), + ("\bki+ke+s*\b", None, False), + ("\bbeaner+s?\b", None, False), + ("\bcoo+ns*\b", None, False), + ("\bnig+lets*\b", None, False), + ("\bslant-eyes*\b", None, False), + ("\btowe?l-?head+s*\b", None, False), + ("\bchi*n+k+s*\b", None, False), + ("\bspick*s*\b", None, False), + ("\bkill* +(?:yo)?urself+\b", None, False), + ("\bjew+s*\b", None, False), + ("\bsuicide\b", None, False), + ("\brape\b", None, False), + ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), + ("\bta+r+d+\b", None, False), + ("\bcunts*\b", None, False), + ("\btrann*y\b", None, False), + ("\bshemale\b", None, False), + ("fa+g+s*", None, False), + ("卐", None, False), + ("卍", None, False), + ("࿖", None, False), + ("࿕", None, False), + ("࿘", None, False), + ("࿗", None, False), + ("cuck(?!oo+)", None, False), + ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), + ("fag+o+t+s*", None, False), +] + +file_format_whitelist = [ + (".3gp", None, True), + (".3g2", None, True), + (".avi", None, True), + (".bmp", None, True), + (".gif", None, True), + (".h264", None, True), + (".jpg", None, True), + (".jpeg", None, True), + (".m4v", None, True), + (".mkv", None, True), + (".mov", None, True), + (".mp4", None, True), + (".mpeg", None, True), + (".mpg", None, True), + (".png", None, True), + (".tiff", None, True), + (".wmv", None, True), + (".svg", None, True), + (".psd", "Photoshop", True), + (".ai", "Illustrator", True), + (".aep", "After Effects", True), + (".xcf", "GIMP", True), + (".mp3", None, True), + (".wav", None, True), + (".ogg", None, True), + (".webm", None, True), + (".webp", None, True), +] + +populate_data = { + "FILTER_TOKEN": filter_token_blacklist, + "DOMAIN_NAME": domain_name_blacklist, + "FILE_FORMAT": file_format_whitelist, + "GUILD_INVITE": guild_invite_whitelist, +} + + +class Migration(migrations.Migration): + dependencies = [("api", "0058_create_new_filterlist_model")] + + def populate_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + + for filterlist_type, metadata in populate_data.items(): + for content, comment, allowed in metadata: + FilterList.objects.create( + type=filterlist_type, + allowed=allowed, + content=content, + comment=comment, + ) + + def clear_filterlists(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.all().delete() + + operations = [ + migrations.RunPython(populate_filterlists, clear_filterlists) + ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists.py b/pydis_site/apps/api/migrations/0060_populate_filterlists.py deleted file mode 100644 index 35fde95a..00000000 --- a/pydis_site/apps/api/migrations/0060_populate_filterlists.py +++ /dev/null @@ -1,153 +0,0 @@ -from django.db import migrations - -guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -domain_name_blacklist = [ - ("pornhub.com", None, False), - ("liveleak.com", None, False), - ("grabify.link", None, False), - ("bmwforum.co", None, False), - ("leancoding.co", None, False), - ("spottyfly.com", None, False), - ("stopify.co", None, False), - ("yoütu.be", None, False), - ("discörd.com", None, False), - ("minecräft.com", None, False), - ("freegiftcards.co", None, False), - ("disçordapp.com", None, False), - ("fortnight.space", None, False), - ("fortnitechat.site", None, False), - ("joinmy.site", None, False), - ("curiouscat.club", None, False), - ("catsnthings.fun", None, False), - ("yourtube.site", None, False), - ("youtubeshort.watch", None, False), - ("catsnthing.com", None, False), - ("youtubeshort.pro", None, False), - ("canadianlumberjacks.online", None, False), - ("poweredbydialup.club", None, False), - ("poweredbydialup.online", None, False), - ("poweredbysecurity.org", None, False), - ("poweredbysecurity.online", None, False), - ("ssteam.site", None, False), - ("steamwalletgift.com", None, False), - ("discord.gift", None, False), - ("lmgtfy.com", None, False), -] - -filter_token_blacklist = [ - ("\bgoo+ks*\b", None, False), - ("\bky+s+\b", None, False), - ("\bki+ke+s*\b", None, False), - ("\bbeaner+s?\b", None, False), - ("\bcoo+ns*\b", None, False), - ("\bnig+lets*\b", None, False), - ("\bslant-eyes*\b", None, False), - ("\btowe?l-?head+s*\b", None, False), - ("\bchi*n+k+s*\b", None, False), - ("\bspick*s*\b", None, False), - ("\bkill* +(?:yo)?urself+\b", None, False), - ("\bjew+s*\b", None, False), - ("\bsuicide\b", None, False), - ("\brape\b", None, False), - ("\b(re+)tar+(d+|t+)(ed)?\b", None, False), - ("\bta+r+d+\b", None, False), - ("\bcunts*\b", None, False), - ("\btrann*y\b", None, False), - ("\bshemale\b", None, False), - ("fa+g+s*", None, False), - ("卐", None, False), - ("卍", None, False), - ("࿖", None, False), - ("࿕", None, False), - ("࿘", None, False), - ("࿗", None, False), - ("cuck(?!oo+)", None, False), - ("nigg+(?:e*r+|a+h*?|u+h+)s?", None, False), - ("fag+o+t+s*", None, False), -] - -file_format_whitelist = [ - (".3gp", None, True), - (".3g2", None, True), - (".avi", None, True), - (".bmp", None, True), - (".gif", None, True), - (".h264", None, True), - (".jpg", None, True), - (".jpeg", None, True), - (".m4v", None, True), - (".mkv", None, True), - (".mov", None, True), - (".mp4", None, True), - (".mpeg", None, True), - (".mpg", None, True), - (".png", None, True), - (".tiff", None, True), - (".wmv", None, True), - (".svg", None, True), - (".psd", "Photoshop", True), - (".ai", "Illustrator", True), - (".aep", "After Effects", True), - (".xcf", "GIMP", True), - (".mp3", None, True), - (".wav", None, True), - (".ogg", None, True), - (".webm", None, True), - (".webp", None, True), -] - -populate_data = { - "FILTER_TOKEN": filter_token_blacklist, - "DOMAIN_NAME": domain_name_blacklist, - "FILE_FORMAT": file_format_whitelist, - "GUILD_INVITE": guild_invite_whitelist, -} - - -class Migration(migrations.Migration): - dependencies = [("api", "0059_create_new_filterlist_model")] - - def populate_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - - for filterlist_type, metadata in populate_data.items(): - for content, comment, allowed in metadata: - FilterList.objects.create( - type=filterlist_type, - allowed=allowed, - content=content, - comment=comment, - ) - - def clear_filterlists(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.all().delete() - - operations = [ - migrations.RunPython(populate_filterlists, clear_filterlists) - ] diff --git a/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py new file mode 100644 index 00000000..53846f02 --- /dev/null +++ b/pydis_site/apps/api/migrations/0060_populate_filterlists_fix.py @@ -0,0 +1,85 @@ +from django.db import migrations + +bad_guild_invite_whitelist = [ + ("discord.gg/python", "Python Discord", True), + ("discord.gg/4JJdJKb", "RLBot", True), + ("discord.gg/djPtTRJ", "Kivy", True), + ("discord.gg/QXyegWe", "Pyglet", True), + ("discord.gg/9XsucTT", "Panda3D", True), + ("discord.gg/AP3rq2k", "PyWeek", True), + ("discord.gg/vSPsP9t", "Microsoft Python", True), + ("discord.gg/bRCvFy9", "Discord.js Official", True), + ("discord.gg/9zT7NHP", "Programming Discussions", True), + ("discord.gg/ysd6M4r", "JetBrains Community", True), + ("discord.gg/4xJeCgy", "Raspberry Pie", True), + ("discord.gg/AStb3kZ", "Ren'Py", True), + ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), + ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), + ("discord.gg/jTtgWuy", "Django", True), + ("discord.gg/W9BypZF", "STEM", True), + ("discord.gg/dpy", "discord.py", True), + ("discord.gg/programming", "Programmers Hangout", True), + ("discord.gg/qhGUjGD", "SpeakJS", True), + ("discord.gg/eTbWSZj", "Functional Programming", True), + ("discord.gg/r8yreB6", "PyGame", True), + ("discord.gg/5UBnR3P", "Python Atlanta", True), + ("discord.gg/ccyrDKv", "C#", True), +] + +guild_invite_whitelist = [ + ("267624335836053506", "Python Discord", True), + ("348658686962696195", "RLBot", True), + ("423249981340778496", "Kivy", True), + ("438622377094414346", "Pyglet", True), + ("524691714909274162", "Panda3D", True), + ("666560367173828639", "PyWeek", True), + ("702724176489873509", "Microsoft Python", True), + ("222078108977594368", "Discord.js Official", True), + ("238666723824238602", "Programming Discussions", True), + ("433980600391696384", "JetBrains Community", True), + ("204621105720328193", "Raspberry Pie", True), + ("286633898581164032", "Ren'Py", True), + ("440186186024222721", "Python Discord: Emojis 1", True), + ("578587418123304970", "Python Discord: Emojis 2", True), + ("159039020565790721", "Django", True), + ("273944235143593984", "STEM", True), + ("336642139381301249", "discord.py", True), + ("244230771232079873", "Programmers Hangout", True), + ("239433591950540801", "SpeakJS", True), + ("280033776820813825", "Functional Programming", True), + ("349505959032389632", "PyGame", True), + ("488751051629920277", "Python Atlanta", True), + ("143867839282020352", "C#", True), +] + + +class Migration(migrations.Migration): + dependencies = [("api", "0059_populate_filterlists")] + + def fix_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. + + for content, comment, allowed in guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + def restore_bad_filterlist(app, _): + FilterList = app.get_model("api", "FilterList") + FilterList.objects.filter(type="GUILD_INVITE").delete() + + for content, comment, allowed in bad_guild_invite_whitelist: + FilterList.objects.create( + type="GUILD_INVITE", + allowed=allowed, + content=content, + comment=comment, + ) + + operations = [ + migrations.RunPython(fix_filterlist, restore_bad_filterlist) + ] diff --git a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py b/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py deleted file mode 100644 index eaaafb38..00000000 --- a/pydis_site/apps/api/migrations/0061_populate_filterlists_fix.py +++ /dev/null @@ -1,85 +0,0 @@ -from django.db import migrations - -bad_guild_invite_whitelist = [ - ("discord.gg/python", "Python Discord", True), - ("discord.gg/4JJdJKb", "RLBot", True), - ("discord.gg/djPtTRJ", "Kivy", True), - ("discord.gg/QXyegWe", "Pyglet", True), - ("discord.gg/9XsucTT", "Panda3D", True), - ("discord.gg/AP3rq2k", "PyWeek", True), - ("discord.gg/vSPsP9t", "Microsoft Python", True), - ("discord.gg/bRCvFy9", "Discord.js Official", True), - ("discord.gg/9zT7NHP", "Programming Discussions", True), - ("discord.gg/ysd6M4r", "JetBrains Community", True), - ("discord.gg/4xJeCgy", "Raspberry Pie", True), - ("discord.gg/AStb3kZ", "Ren'Py", True), - ("discord.gg/t655QNV", "Python Discord: Emojis 1", True), - ("discord.gg/vRZPkqC", "Python Discord: Emojis 2", True), - ("discord.gg/jTtgWuy", "Django", True), - ("discord.gg/W9BypZF", "STEM", True), - ("discord.gg/dpy", "discord.py", True), - ("discord.gg/programming", "Programmers Hangout", True), - ("discord.gg/qhGUjGD", "SpeakJS", True), - ("discord.gg/eTbWSZj", "Functional Programming", True), - ("discord.gg/r8yreB6", "PyGame", True), - ("discord.gg/5UBnR3P", "Python Atlanta", True), - ("discord.gg/ccyrDKv", "C#", True), -] - -guild_invite_whitelist = [ - ("267624335836053506", "Python Discord", True), - ("348658686962696195", "RLBot", True), - ("423249981340778496", "Kivy", True), - ("438622377094414346", "Pyglet", True), - ("524691714909274162", "Panda3D", True), - ("666560367173828639", "PyWeek", True), - ("702724176489873509", "Microsoft Python", True), - ("222078108977594368", "Discord.js Official", True), - ("238666723824238602", "Programming Discussions", True), - ("433980600391696384", "JetBrains Community", True), - ("204621105720328193", "Raspberry Pie", True), - ("286633898581164032", "Ren'Py", True), - ("440186186024222721", "Python Discord: Emojis 1", True), - ("578587418123304970", "Python Discord: Emojis 2", True), - ("159039020565790721", "Django", True), - ("273944235143593984", "STEM", True), - ("336642139381301249", "discord.py", True), - ("244230771232079873", "Programmers Hangout", True), - ("239433591950540801", "SpeakJS", True), - ("280033776820813825", "Functional Programming", True), - ("349505959032389632", "PyGame", True), - ("488751051629920277", "Python Atlanta", True), - ("143867839282020352", "C#", True), -] - - -class Migration(migrations.Migration): - dependencies = [("api", "0060_populate_filterlists")] - - def fix_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() # Clear out the stuff added in 0059. - - for content, comment, allowed in guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - def restore_bad_filterlist(app, _): - FilterList = app.get_model("api", "FilterList") - FilterList.objects.filter(type="GUILD_INVITE").delete() - - for content, comment, allowed in bad_guild_invite_whitelist: - FilterList.objects.create( - type="GUILD_INVITE", - allowed=allowed, - content=content, - comment=comment, - ) - - operations = [ - migrations.RunPython(fix_filterlist, restore_bad_filterlist) - ] -- cgit v1.2.3 From ab634c6ef1d4b7cec3b8471262eec3ec727478d8 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 30 Aug 2020 08:27:07 +0300 Subject: Create merge migration to fix conflicts --- pydis_site/apps/api/migrations/0061_merge_20200830_0526.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0061_merge_20200830_0526.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0061_merge_20200830_0526.py b/pydis_site/apps/api/migrations/0061_merge_20200830_0526.py new file mode 100644 index 00000000..f0668696 --- /dev/null +++ b/pydis_site/apps/api/migrations/0061_merge_20200830_0526.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.8 on 2020-08-30 05:26 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0060_populate_filterlists_fix'), + ('api', '0052_offtopicchannelname_used'), + ] + + operations = [ + ] -- cgit v1.2.3 From a9e1f6f398ca4cac42c01a211513c02881e7541d Mon Sep 17 00:00:00 2001 From: Karlis S Date: Tue, 1 Sep 2020 14:59:23 +0000 Subject: Create new merge migration to fix conflicts between migrations --- pydis_site/apps/api/migrations/0062_merge_20200901_1459.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pydis_site/apps/api/migrations/0062_merge_20200901_1459.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0062_merge_20200901_1459.py b/pydis_site/apps/api/migrations/0062_merge_20200901_1459.py new file mode 100644 index 00000000..d162acf1 --- /dev/null +++ b/pydis_site/apps/api/migrations/0062_merge_20200901_1459.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.8 on 2020-09-01 14:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0051_delete_tag'), + ('api', '0061_merge_20200830_0526'), + ] + + operations = [ + ] -- cgit v1.2.3 From 01a0e825333812dff21351201fce8c25031cc8fa Mon Sep 17 00:00:00 2001 From: Leon Sandøy Date: Thu, 3 Sep 2020 00:39:25 +0200 Subject: Update landing page. Replace flake8-annotations with metricity. --- pydis_site/apps/home/tests/mock_github_api_response.json | 2 +- pydis_site/apps/home/views/home.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/home/tests/mock_github_api_response.json b/pydis_site/apps/home/tests/mock_github_api_response.json index 35604a85..10be4f99 100644 --- a/pydis_site/apps/home/tests/mock_github_api_response.json +++ b/pydis_site/apps/home/tests/mock_github_api_response.json @@ -28,7 +28,7 @@ "forks_count": 31 }, { - "full_name": "python-discord/flake8-annotations", + "full_name": "python-discord/metricity", "description": "test", "stargazers_count": 97, "language": "Python", diff --git a/pydis_site/apps/home/views/home.py b/pydis_site/apps/home/views/home.py index 20e38ab0..3b5cd5ac 100644 --- a/pydis_site/apps/home/views/home.py +++ b/pydis_site/apps/home/views/home.py @@ -23,7 +23,7 @@ class HomeView(View): "python-discord/bot", "python-discord/snekbox", "python-discord/seasonalbot", - "python-discord/flake8-annotations", + "python-discord/metricity", "python-discord/django-simple-bulma", ] -- cgit v1.2.3 From 49e7243f6527140dbdb29a2f1dd994839f248dba Mon Sep 17 00:00:00 2001 From: Eivind Teig Date: Fri, 11 Sep 2020 23:43:06 +0200 Subject: Allow blank/null input to the nomination reason. We are lowering the threshold for nomination. By allowing the users to make a nomination without a reason might make this feature more attractive amongst members of staff. --- .../0063_Allow_blank_or_null_for_nomination_reason.py | 18 ++++++++++++++++++ pydis_site/apps/api/models/bot/nomination.py | 4 +++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py (limited to 'pydis_site') diff --git a/pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py b/pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py new file mode 100644 index 00000000..9eb05eaa --- /dev/null +++ b/pydis_site/apps/api/migrations/0063_Allow_blank_or_null_for_nomination_reason.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.9 on 2020-09-11 21:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0062_merge_20200901_1459'), + ] + + operations = [ + migrations.AlterField( + model_name='nomination', + name='reason', + field=models.TextField(blank=True, help_text='Why this user was nominated.', null=True), + ), + ] diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py index 21e34e87..183b22d5 100644 --- a/pydis_site/apps/api/models/bot/nomination.py +++ b/pydis_site/apps/api/models/bot/nomination.py @@ -18,7 +18,9 @@ class Nomination(ModelReprMixin, models.Model): related_name='nomination_set' ) reason = models.TextField( - help_text="Why this user was nominated." + help_text="Why this user was nominated.", + null=True, + blank=True ) user = models.ForeignKey( User, -- cgit v1.2.3 From 1ed224135e48d5d1840d14d446575b8cbc5a7a6e Mon Sep 17 00:00:00 2001 From: Eivind Teig Date: Sat, 12 Sep 2020 00:13:32 +0200 Subject: Fix a broken test for nomination reason. We no longer return 400 if a reason is missing. --- pydis_site/apps/api/tests/test_nominations.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'pydis_site') diff --git a/pydis_site/apps/api/tests/test_nominations.py b/pydis_site/apps/api/tests/test_nominations.py index 92c62c87..b37135f8 100644 --- a/pydis_site/apps/api/tests/test_nominations.py +++ b/pydis_site/apps/api/tests/test_nominations.py @@ -80,7 +80,7 @@ class CreationTests(APISubdomainTestCase): 'actor': ['This field is required.'] }) - def test_returns_400_for_missing_reason(self): + def test_returns_201_for_missing_reason(self): url = reverse('bot:nomination-list', host='api') data = { 'user': self.user.id, @@ -88,10 +88,7 @@ class CreationTests(APISubdomainTestCase): } response = self.client.post(url, data=data) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - 'reason': ['This field is required.'] - }) + self.assertEqual(response.status_code, 201) def test_returns_400_for_bad_user(self): url = reverse('bot:nomination-list', host='api') -- cgit v1.2.3