diff --git a/aiarena/core/management/commands/seed.py b/aiarena/core/management/commands/seed.py index febf7edc..f40c93e9 100644 --- a/aiarena/core/management/commands/seed.py +++ b/aiarena/core/management/commands/seed.py @@ -5,13 +5,13 @@ from aiarena import settings from aiarena.core.models import User, Map, Bot, News, \ - CompetitionParticipation, MapPool + CompetitionParticipation, MapPool, WebsiteUser from aiarena.core.tests.testing_utils import TestingClient from aiarena.core.tests.tests import BaseTestMixin from aiarena.core.utils import EnvironmentType def run_seed(matches, token): - devadmin = User.objects.create_superuser(username='devadmin', password='x', email='devadmin@dev.aiarena.net') + devadmin = WebsiteUser.objects.create_superuser(username='devadmin', password='x', email='devadmin@dev.aiarena.net') client = TestingClient() client.login(devadmin) diff --git a/aiarena/core/migrations/0029_websiteuser.py b/aiarena/core/migrations/0029_websiteuser.py new file mode 100644 index 00000000..77215684 --- /dev/null +++ b/aiarena/core/migrations/0029_websiteuser.py @@ -0,0 +1,38 @@ +# Generated by Django 3.0.14 on 2021-05-05 22:40 + +from django.conf import settings +import django.contrib.auth.models +from django.db import migrations, models +import django.db.models.deletion + + +def migrate_website_users(apps, schema_editor): + User = apps.get_model('core', 'User') + WebsiteUser = apps.get_model('core', 'WebsiteUser') + for website_user in User.objects.filter(type='WEBSITE_USER'): + website_user.__class__ = WebsiteUser # convert to new class + website_user.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0028_auto_20210419_2336'), + ] + + operations = [ + migrations.CreateModel( + name='WebsiteUser', + fields=[ + ('user_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ('single_use_match_requests', models.IntegerField(blank=True, default=0)), + ], + options={ + 'verbose_name': 'WebsiteUser', + }, + bases=('core.user',), + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.RunPython(migrate_website_users), + ] diff --git a/aiarena/core/models/__init__.py b/aiarena/core/models/__init__.py index a9cddc38..fcd2f70b 100644 --- a/aiarena/core/models/__init__.py +++ b/aiarena/core/models/__init__.py @@ -15,7 +15,8 @@ from .relative_result import RelativeResult from .result import Result from .round import Round +from .tag import Tag from .trophy import Trophy from .trophy import TrophyIcon -from .tag import Tag from .user import User +from .website_user import WebsiteUser diff --git a/aiarena/core/models/user.py b/aiarena/core/models/user.py index e1e5c79e..4865f8ee 100644 --- a/aiarena/core/models/user.py +++ b/aiarena/core/models/user.py @@ -2,21 +2,22 @@ from constance import config from django.contrib.auth.models import AbstractUser -from django.core.exceptions import ValidationError from django.db import models from django.db.models.signals import pre_save from django.dispatch import receiver from django.urls import reverse from django.utils import timezone +from django.utils.functional import cached_property from django.utils.html import escape from django.utils.safestring import mark_safe -from django.utils.functional import cached_property - from django.utils.translation import gettext_lazy as _ + +from aiarena.core.models.mixins import LockableModelMixin + logger = logging.getLogger(__name__) -class User(AbstractUser): +class User(AbstractUser, LockableModelMixin): PATREON_LEVELS = ( ('none', 'None'), ('bronze', 'Bronze'), @@ -120,9 +121,16 @@ def is_arenaclient(self): except ArenaClient.DoesNotExist: return False + @property + def is_websiteuser(self): + from .website_user import WebsiteUser # avoid circular reference + try: + return (self.websiteuser is not None) + except WebsiteUser.DoesNotExist: + return False @receiver(pre_save, sender=User) def pre_save_user(sender, instance, **kwargs): - if instance.type != 'WEBSITE_USER': + if not instance.is_websiteuser: instance.set_unusable_password() diff --git a/aiarena/core/models/website_user.py b/aiarena/core/models/website_user.py new file mode 100644 index 00000000..eb27b7a4 --- /dev/null +++ b/aiarena/core/models/website_user.py @@ -0,0 +1,35 @@ +import logging + +from constance import config +from django.db import models +from django.utils import timezone + +from .user import User + +logger = logging.getLogger(__name__) + + +class WebsiteUser(User): + """Represents a website user/bot author""" + single_use_match_requests = models.IntegerField(default=0, blank=True) + """UNUSED AS OF YET""" + """Single-use match requests that go on top of any periodic match requests a user might have. + Periodic match requests are used first before these.""" + + @property + def requested_matches_limit(self): + return self.REQUESTED_MATCHES_LIMIT_MAP[self.patreon_level] + self.extra_periodic_match_requests + + @property + def match_request_count_left(self): + from .match import Match + from .result import Result + return self.requested_matches_limit \ + - Match.objects.only('id').filter(requested_by=self, + created__gte=timezone.now() - config.REQUESTED_MATCHES_LIMIT_PERIOD).count() \ + + Result.objects.only('id').filter(submitted_by=self, type='MatchCancelled', + created__gte=timezone.now() - config.REQUESTED_MATCHES_LIMIT_PERIOD).count() + + + class Meta: + verbose_name = 'WebsiteUser' diff --git a/aiarena/core/tests/tests.py b/aiarena/core/tests/tests.py index 270a2162..78de500c 100644 --- a/aiarena/core/tests/tests.py +++ b/aiarena/core/tests/tests.py @@ -14,7 +14,7 @@ from aiarena.core.api import Matches from aiarena.core.management.commands import cleanupreplays from aiarena.core.models import User, Bot, Map, Match, Result, MatchParticipation, Competition, Round, ArenaClient, \ - CompetitionParticipation, MapPool, MatchTag, Tag + CompetitionParticipation, MapPool, WebsiteUser, Tag from aiarena.core.models.game_mode import GameMode from aiarena.core.utils import calculate_md5 @@ -307,11 +307,11 @@ def _generate_extra_bots(self): def _generate_extra_users(self): - self.regularUser2 = User.objects.create_user(username='regular_user2', password='x', + self.regularUser2 = WebsiteUser.objects.create_user(username='regular_user2', password='x', email='regular_user2@dev.aiarena.net') - self.regularUser3 = User.objects.create_user(username='regular_user3', password='x', + self.regularUser3 = WebsiteUser.objects.create_user(username='regular_user3', password='x', email='regular_user3@dev.aiarena.net') - self.regularUser4 = User.objects.create_user(username='regular_user4', password='x', + self.regularUser4 = WebsiteUser.objects.create_user(username='regular_user4', password='x', email='regular_user4@dev.aiarena.net') @@ -323,7 +323,7 @@ class LoggedInMixin(BaseTestMixin): def setUp(self): super().setUp() - self.staffUser1 = User.objects.create_user(username='staff_user', password='x', + self.staffUser1 = WebsiteUser.objects.create_user(username='staff_user', password='x', email='staff_user@dev.aiarena.net', is_staff=True, is_superuser=True, @@ -333,7 +333,7 @@ def setUp(self): type='ARENA_CLIENT', trusted=True, owner=self.staffUser1) Token.objects.create(user=self.arenaclientUser1) - self.regularUser1 = User.objects.create_user(username='regular_user1', password='x', + self.regularUser1 = WebsiteUser.objects.create_user(username='regular_user1', password='x', email='regular_user1@dev.aiarena.net') @@ -489,7 +489,7 @@ def _send_tags(self, bot1_tags, bot2_tags, results_resp_code=201): def test_results_with_tags(self): az_symbols = 'abcdefghijklmnopqrstuvwxyz' num_symbols = '0123456789' - extra_symbols = ' _ _ ' + extra_symbols = ' _ _ ' game_mode = GameMode.objects.first() self.client.force_login(self.arenaclientUser1) @@ -505,14 +505,14 @@ def test_results_with_tags(self): match_tags = Match.objects.get(id=match_response.data['id']).tags.all() self.assertTrue(match_tags.count()==1) for mt in match_tags: - self.assertEqual(mt.user, self.staffUser1) + self.assertEqual(mt.user.websiteuser, self.staffUser1) Matches.request_match(self.staffUser1, self.staffUser1Bot2, self.regularUser1Bot1, game_mode=game_mode) match_response, result_response = self._send_tags(None, ['abc']) match_tags = Match.objects.get(id=match_response.data['id']).tags.all() self.assertTrue(match_tags.count()==1) for mt in match_tags: - self.assertEqual(mt.user, self.regularUser1) + self.assertEqual(mt.user.websiteuser, self.regularUser1) # Check that tags are correct, stripped and attributed to the correct user _temp_tag1 = 'tes1t_ test2' @@ -562,7 +562,7 @@ def test_results_with_tags(self): # This is to prevent tags from causing a result to fail submission Matches.request_match(self.staffUser1, self.staffUser1Bot2, self.regularUser1Bot1, game_mode=game_mode) match_response, result_response = self._send_tags( - bot1_tags=['!', '2', 'A', '', az_symbols+num_symbols+extra_symbols], + bot1_tags=['!', '2', 'A', '', az_symbols+num_symbols+extra_symbols], bot2_tags=['123'] ) match_tags = Match.objects.get(id=match_response.data['id']).tags.all() @@ -572,13 +572,13 @@ def test_results_with_tags(self): # Too many tags Matches.request_match(self.staffUser1, self.staffUser1Bot2, self.regularUser1Bot1, game_mode=game_mode) match_response, result_response = self._send_tags( - bot1_tags=[str(i) for i in range(50)], + bot1_tags=[str(i) for i in range(50)], bot2_tags=[str(i) for i in range(50)] ) match_tags = Match.objects.get(id=match_response.data['id']).tags.all() self.assertTrue(match_tags.count()==64) - + class CompetitionsTestCase(FullDataSetMixin, TransactionTestCase): diff --git a/aiarena/frontend/admin.py b/aiarena/frontend/admin.py index bda68cc7..37a714c0 100644 --- a/aiarena/frontend/admin.py +++ b/aiarena/frontend/admin.py @@ -3,7 +3,7 @@ from aiarena.core.models import ArenaClient, Bot, Map, Match, MatchParticipation, Result, Round, Competition, \ CompetitionBotMatchupStats, CompetitionParticipation, Trophy, TrophyIcon, User, News, MapPool, MatchTag, Tag, \ - ArenaClientStatus + ArenaClientStatus, WebsiteUser from aiarena.core.models.game import Game from aiarena.core.models.game_mode import GameMode from aiarena.patreon.models import PatreonAccountBind @@ -409,3 +409,38 @@ class UserAdmin(admin.ModelAdmin): 'can_request_games_for_another_authors_bot', ) raw_id_fields = ('groups', 'user_permissions') + + +@admin.register(WebsiteUser) +class WebsiteUserAdmin(admin.ModelAdmin): + search_fields = ('username',) + list_display = ( + 'id', + 'password', + 'last_login', + 'is_superuser', + 'username', + 'first_name', + 'last_name', + 'is_staff', + 'is_active', + 'date_joined', + 'email', + 'patreon_level', + 'type', + 'extra_active_competition_participations', + 'extra_periodic_match_requests', + 'receive_email_comms', + 'can_request_games_for_another_authors_bot', + 'single_use_match_requests' + ) + list_filter = ( + 'last_login', + 'is_superuser', + 'is_staff', + 'is_active', + 'date_joined', + 'receive_email_comms', + 'can_request_games_for_another_authors_bot', + ) + raw_id_fields = ('groups', 'user_permissions') diff --git a/aiarena/frontend/templates/500.html b/aiarena/frontend/templates/500.html index 7e54f264..d6728992 100644 --- a/aiarena/frontend/templates/500.html +++ b/aiarena/frontend/templates/500.html @@ -2,5 +2,5 @@ {% block content %}

Whoops...

-
There was some error ¯\_(ツ)_/¯
+
There was some error ¯\_(ツ)_/¯

Please contact our staff if this issue continues so we can investigate.
{% endblock %} \ No newline at end of file diff --git a/aiarena/frontend/views.py b/aiarena/frontend/views.py index 0cbc5b60..6a84fd87 100644 --- a/aiarena/frontend/views.py +++ b/aiarena/frontend/views.py @@ -986,7 +986,7 @@ def _get_map(self, form): def form_valid(self, form): if config.ALLOW_REQUESTED_MATCHES: if form.cleaned_data['bot1'] != form.cleaned_data['bot2']: - if self.request.user.match_request_count_left >= form.cleaned_data['match_count']: + if self.request.user.websiteuser.match_request_count_left >= form.cleaned_data['match_count']: with transaction.atomic(): # do this all in one commit match_list = []