From 4a8babf39e328044fe30faa13a2a395b25c3358c Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sat, 18 May 2024 09:51:44 -0500 Subject: [PATCH] enforce exclusivity of only one active Game at a time a prior commit made this determination for now, for simplicity's sake (and also what are the odds of running two games at once on the same codebase), but it was'n really enforced until now Signed-off-by: Brian S. Stephan --- idlerpg/ircplugin.py | 25 +++++++++---------- .../0008_game_one_enabled_at_a_time.py | 18 +++++++++++++ idlerpg/models.py | 3 ++- tests/fixtures/simple_character.json | 11 +------- tests/test_idlerpg_game.py | 16 ++++++++++++ tests/test_idlerpg_ircplugin.py | 16 ++++++------ 6 files changed, 57 insertions(+), 32 deletions(-) create mode 100644 idlerpg/migrations/0008_game_one_enabled_at_a_time.py diff --git a/idlerpg/ircplugin.py b/idlerpg/ircplugin.py index 6fb2063..898dc7e 100644 --- a/idlerpg/ircplugin.py +++ b/idlerpg/ircplugin.py @@ -180,20 +180,19 @@ class IdleRPG(Plugin): """Check for characters who have leveled up, and log as such in the channel.""" while self.check_for_level_ups: time.sleep(self.SLEEP_BETWEEN_LEVEL_CHECKS) - self._level_up_games() + self._level_up_characters() - def _level_up_games(self): - """Find games under management and level up characters within them, updating their channel.""" - logger.debug("checking for level ups in games") - for game in Game.objects.filter(active=True): - logger.debug("checking for level ups in %s", game) - for character in Character.objects.levelable(game): - logger.debug("going to try to level up %s", character) - character.level_up() - self.bot.reply(None, f"{character.name}, the {character.character_class}, has attained level " - f"{character.level}! Next level at {character.next_level_str()}.", - explicit_target=game.channel.name) - character.save() + def _level_up_characters(self): + """Level up characters in the active game, updating their channel.""" + game = Game.objects.get(active=True) + logger.debug("checking for level ups in %s", game) + for character in Character.objects.levelable(game): + logger.debug("going to try to level up %s", character) + character.level_up() + self.bot.reply(None, f"{character.name}, the {character.character_class}, has attained level " + f"{character.level}! Next level at {character.next_level_str()}.", + explicit_target=game.channel.name) + character.save() plugin = IdleRPG diff --git a/idlerpg/migrations/0008_game_one_enabled_at_a_time.py b/idlerpg/migrations/0008_game_one_enabled_at_a_time.py new file mode 100644 index 0000000..dc10051 --- /dev/null +++ b/idlerpg/migrations/0008_game_one_enabled_at_a_time.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.5 on 2024-05-18 14:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('idlerpg', '0007_remove_character_one_player_character_per_game_and_more'), + ('ircbot', '0020_alter_alias_id_alter_botuser_id_alter_ircchannel_id_and_more'), + ] + + operations = [ + migrations.AddConstraint( + model_name='game', + constraint=models.UniqueConstraint(models.F('active'), condition=models.Q(('active', True)), name='one_enabled_at_a_time'), + ), + ] diff --git a/idlerpg/models.py b/idlerpg/models.py index 8edadf2..d973ef2 100644 --- a/idlerpg/models.py +++ b/idlerpg/models.py @@ -26,7 +26,8 @@ class Game(models.Model): """Options for the Game and its objects.""" constraints = [ - models.UniqueConstraint("active", "channel", name="one_game_per_channel"), + models.UniqueConstraint('active', 'channel', name='one_game_per_channel'), + models.UniqueConstraint('active', condition=models.Q(active=True), name='one_enabled_at_a_time'), ] def __str__(self): diff --git a/tests/fixtures/simple_character.json b/tests/fixtures/simple_character.json index 3c16394..ab99def 100644 --- a/tests/fixtures/simple_character.json +++ b/tests/fixtures/simple_character.json @@ -37,7 +37,7 @@ "model": "ircbot.ircchannel", "pk": 2, "fields": { - "name": "#level_test", + "name": "#duplicate_test", "server": 1, "autojoin": false, "topic_msg": "", @@ -56,15 +56,6 @@ "channel": 1 } }, -{ - "model": "idlerpg.game", - "pk": 2, - "fields": { - "name": "level test", - "active": true, - "channel": 2 - } -}, { "model": "idlerpg.character", "pk": 1, diff --git a/tests/test_idlerpg_game.py b/tests/test_idlerpg_game.py index 6672dcd..b1e7da7 100644 --- a/tests/test_idlerpg_game.py +++ b/tests/test_idlerpg_game.py @@ -5,7 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-or-later """ import logging +from django.db import transaction +from django.db.utils import IntegrityError from django.test import TestCase +from ircbot.models import IrcChannel from idlerpg.models import Game @@ -22,3 +25,16 @@ class GameTest(TestCase): game = Game.objects.get(pk=1) logger.debug(str(game)) assert str(game) == "test in #test on default (active)" + + def test_cant_have_two_active(self): + """Test that if we create another game, it's disabled, and can't be active until the first is disabled.""" + channel = IrcChannel.objects.get(pk=2) + game = Game.objects.get(pk=1) + new_game = Game.objects.create(name='new one', channel=channel) + + assert game.active is True + assert new_game.active is False + with self.assertRaises(IntegrityError): + with transaction.atomic(): + new_game.active = True + new_game.save() diff --git a/tests/test_idlerpg_ircplugin.py b/tests/test_idlerpg_ircplugin.py index e9e892b..2e17547 100644 --- a/tests/test_idlerpg_ircplugin.py +++ b/tests/test_idlerpg_ircplugin.py @@ -262,7 +262,7 @@ class IrcPluginTest(TestCase): mock_event = mock.MagicMock() mock_event.source = 'bss!bss@bss_login' mock_event.recursing = False - game = Game.objects.get(pk=2) + game = Game.objects.get(pk=1) test_char = Character.objects.register('test_login', game, 'test', 'bss!bss@bss_login', 'tester') test_char.log_out() test_char.next_level = datetime.datetime.fromisoformat('2024-05-17 17:00:00-00:00') @@ -283,7 +283,7 @@ class IrcPluginTest(TestCase): # the ordering of this surprises me... keep an eye on it self.mock_bot.reply.assert_has_calls([ mock.call(None, "test_login, the level 0 tester, is now online from nickname bss. " - "Next level at 2024-05-17 17:00:00 UTC.", explicit_target='#level_test'), + "Next level at 2024-05-17 17:00:00 UTC.", explicit_target='#test'), mock.call(mock_event, "test_login, the level 0 tester, has been successfully logged in."), ]) @@ -317,7 +317,7 @@ class IrcPluginTest(TestCase): mock_event = mock.MagicMock() mock_event.source = 'bss!bss@bss_logout' mock_event.recursing = False - game = Game.objects.get(pk=2) + game = Game.objects.get(pk=1) test_char = Character.objects.register('test_logout', game, 'test', 'bss!bss@bss_logout', 'tester') match = re.match(IdleRPG.LOGOUT_COMMAND_PATTERN, 'LOGOUT') @@ -338,7 +338,7 @@ class IrcPluginTest(TestCase): mock_event.source = 'bss!bss@bss_remove' mock_event.recursing = False - game = Game.objects.get(pk=2) + game = Game.objects.get(pk=1) test_char = Character.objects.register('test_remove', game, 'test', 'bss!bss@bss_remove', 'tester') match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'REMOVEME') self.plugin.handle_remove(self.mock_connection, mock_event, match) @@ -356,7 +356,7 @@ class IrcPluginTest(TestCase): mock_event.source = 'bss2!bss@bss_remove' mock_event.recursing = False - game = Game.objects.get(pk=2) + game = Game.objects.get(pk=1) test_char = Character.objects.register('test_remove', game, 'test', 'bss!bss@bss_remove', 'tester') match = re.match(IdleRPG.LOGIN_COMMAND_PATTERN, 'REMOVEME') self.plugin.handle_remove(self.mock_connection, mock_event, match) @@ -422,9 +422,9 @@ class IrcPluginTest(TestCase): self.plugin.seen_hostmasks.remove('bss3!bss@bss') - def test_level_up_games(self): + def test_level_up_characters(self): """Test the walking of games and characters to level them up.""" - game = Game.objects.get(pk=2) + game = Game.objects.get(pk=1) char_1 = Character.objects.register('test_level_1', game, 'test', 'test_1!test@test', 'tester') char_2 = Character.objects.register('test_level_2', game, 'test', 'test_2!test@test', 'tester') char_2.log_out() @@ -434,7 +434,7 @@ class IrcPluginTest(TestCase): char_1.save() char_2.save() # only one should level up, since char_2 isn't logged in - self.plugin._level_up_games() + self.plugin._level_up_characters() char_1 = Character.objects.get(pk=char_1.pk) char_2 = Character.objects.get(pk=char_2.pk)