diff options
Diffstat (limited to 'pydis_site/apps/api')
22 files changed, 159 insertions, 210 deletions
| 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) 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]), -        ),      ] 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/migrations/0051_allow_blank_message_embeds.py b/pydis_site/apps/api/migrations/0051_allow_blank_message_embeds.py index e617e1c9..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): @@ -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),          ),      ] 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', +        ), +    ] 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/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 = [ +    ] 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 = [ +    ] 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 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 diff --git a/pydis_site/apps/api/models/bot/message.py b/pydis_site/apps/api/models/bot/message.py index 78dcbf1d..f6ae55a5 100644 --- a/pydis_site/apps/api/models/bot/message.py +++ b/pydis_site/apps/api/models/bot/message.py @@ -5,9 +5,9 @@ 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 validate_embed  class Message(ModelReprMixin, models.Model): @@ -47,7 +47,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." 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/models/bot/tag.py b/pydis_site/apps/api/models/utils.py index 5e53582f..107231ba 100644 --- a/pydis_site/apps/api/models/bot/tag.py +++ b/pydis_site/apps/api/models/utils.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: @@ -15,7 +11,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),), @@ -42,7 +38,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': ( @@ -67,7 +63,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': ( @@ -93,7 +89,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. @@ -109,11 +105,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,          ...         )          ...     )          ...     # ... @@ -149,10 +145,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: @@ -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 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.""" 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', 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/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" 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  ) @@ -62,10 +61,6 @@ bot_router.register(      RoleViewSet  )  bot_router.register( -    'tags', -    TagViewSet -) -bot_router.register(      'users',      UserViewSet  ) 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 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/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) 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/<title:str> -    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/<title:str> -    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/<title:str> -    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/<title:str> -    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() | 
