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)