From da1dff29be9fd4a6e5a45317f5426da6372c14fc Mon Sep 17 00:00:00 2001 From: Logan Davidson Date: Sun, 24 Sep 2023 10:20:30 +0100 Subject: [PATCH 01/21] Prepare notification data Prepare notification data for use as a basis for creating senator-notification relations. --- backend/rorapp/functions/face_mortality.py | 2 +- .../0032_change_senator_notification_data.py | 29 +++++++++++++++++++ backend/rorapp/models/notification.py | 2 +- 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 backend/rorapp/migrations/0032_change_senator_notification_data.py diff --git a/backend/rorapp/functions/face_mortality.py b/backend/rorapp/functions/face_mortality.py index 3be128f8..5340c72c 100644 --- a/backend/rorapp/functions/face_mortality.py +++ b/backend/rorapp/functions/face_mortality.py @@ -128,7 +128,7 @@ def face_mortality(game, faction, potential_action, step): step=step, type="face_mortality", faction=senators_former_faction, - data={"senator": senator.id, "major_office": ended_major_office, "heir": heir_id} + data={"senator": senator.id, "major_office": ended_major_office, "heir_senator": heir_id} ) notification.save() diff --git a/backend/rorapp/migrations/0032_change_senator_notification_data.py b/backend/rorapp/migrations/0032_change_senator_notification_data.py new file mode 100644 index 00000000..0ed0ab64 --- /dev/null +++ b/backend/rorapp/migrations/0032_change_senator_notification_data.py @@ -0,0 +1,29 @@ +# This migration does not change models, but it does change data in the database. + +from django.db import migrations + + +# Rename notification data "heir_senator" key to "heir_senator" +# In a future version, notifications might be used as a log of events, so it's better to keep the data consistent. +# Any data key that contains a senator ID should be suffixed with "senator". +def update_notification_data(apps, schema_editor): + Notification = apps.get_model('rorapp', 'Notification') + + face_mortality_notifications = Notification.objects.filter(type='face_mortality') + + for notification in face_mortality_notifications: + + notification.data['heir_senator'] = notification.data['heir'] + del notification.data['heir'] + notification.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('rorapp', '0031_senator_rank_and_faction_rank'), + ] + + operations = [ + migrations.RunPython(update_notification_data, migrations.RunPython.noop), + ] diff --git a/backend/rorapp/models/notification.py b/backend/rorapp/models/notification.py index fc9612d2..07d599d0 100644 --- a/backend/rorapp/models/notification.py +++ b/backend/rorapp/models/notification.py @@ -8,5 +8,5 @@ class Notification(models.Model): index = models.PositiveIntegerField() step = models.ForeignKey(Step, on_delete=models.CASCADE) type = models.CharField(max_length=50) - faction = models.ForeignKey(Faction, on_delete=models.CASCADE, blank=True, null=True, ) + faction = models.ForeignKey(Faction, on_delete=models.CASCADE, blank=True, null=True) data = models.JSONField(blank=True, null=True) From f799cf7f013a2abd5830445c448cead081b91e56 Mon Sep 17 00:00:00 2001 From: Logan Davidson Date: Sun, 24 Sep 2023 11:34:03 +0100 Subject: [PATCH 02/21] Add senator-notification relation Add a senator-notification relation model for tracking relationships between senators and notifications. Fix a bug where the mortality resolution rank assignment was failing. Also fix a bug in mortality notification appearance. --- backend/rorapp/admin/__init__.py | 1 + backend/rorapp/admin/senator.py | 2 +- backend/rorapp/admin/senator_notification.py | 8 ++++ backend/rorapp/functions/face_mortality.py | 16 +++++-- .../functions/rank_senators_and_factions.py | 15 +++--- .../rorapp/functions/select_faction_leader.py | 10 +++- .../migrations/0033_senatornotification.py | 46 +++++++++++++++++++ backend/rorapp/models/__init__.py | 1 + backend/rorapp/models/notification.py | 4 ++ backend/rorapp/models/senator.py | 2 +- backend/rorapp/models/senator_notification.py | 10 ++++ .../Notification_FaceMortality.tsx | 2 +- 12 files changed, 102 insertions(+), 15 deletions(-) create mode 100644 backend/rorapp/admin/senator_notification.py create mode 100644 backend/rorapp/migrations/0033_senatornotification.py create mode 100644 backend/rorapp/models/senator_notification.py diff --git a/backend/rorapp/admin/__init__.py b/backend/rorapp/admin/__init__.py index 2fb0e978..5a1d64db 100644 --- a/backend/rorapp/admin/__init__.py +++ b/backend/rorapp/admin/__init__.py @@ -7,6 +7,7 @@ from .phase import PhaseAdmin from .potential_action import PotentialActionAdmin from .senator import SenatorAdmin +from .senator_notification import SenatorNotificationAdmin from .step import StepAdmin from .title import TitleAdmin from .turn import TurnAdmin diff --git a/backend/rorapp/admin/senator.py b/backend/rorapp/admin/senator.py index e6ec6a96..6d83be78 100644 --- a/backend/rorapp/admin/senator.py +++ b/backend/rorapp/admin/senator.py @@ -5,4 +5,4 @@ # Admin configuration for senators @admin.register(Senator) class SenatorAdmin(admin.ModelAdmin): - list_display = ('id', 'name', 'game', 'faction', 'alive', 'code', 'generation') + list_display = ('id', 'name', 'game', 'faction', 'alive', 'code', 'generation', 'rank') diff --git a/backend/rorapp/admin/senator_notification.py b/backend/rorapp/admin/senator_notification.py new file mode 100644 index 00000000..e24a7293 --- /dev/null +++ b/backend/rorapp/admin/senator_notification.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from rorapp.models import SenatorNotification + + +# Admin configuration for notifications +@admin.register(SenatorNotification) +class SenatorNotificationAdmin(admin.ModelAdmin): + list_display = ('id', 'senator', 'notification') diff --git a/backend/rorapp/functions/face_mortality.py b/backend/rorapp/functions/face_mortality.py index 5340c72c..83a19537 100644 --- a/backend/rorapp/functions/face_mortality.py +++ b/backend/rorapp/functions/face_mortality.py @@ -6,7 +6,7 @@ from asgiref.sync import async_to_sync from rorapp.functions.draw_mortality_chits import draw_mortality_chits from rorapp.functions.rank_senators_and_factions import rank_senators_and_factions -from rorapp.models import Faction, PotentialAction, CompletedAction, Step, Senator, Title, Phase, Turn, Notification +from rorapp.models import Faction, PotentialAction, CompletedAction, Step, Senator, Title, Phase, Turn, Notification, SenatorNotification from rorapp.serializers import NotificationSerializer, PotentialActionSerializer, StepSerializer, TitleSerializer, PhaseSerializer, TurnSerializer, SenatorSerializer @@ -67,7 +67,7 @@ def face_mortality(game, faction, potential_action, step): # End associated titles titles_to_end = Title.objects.filter(senator__id=senator.id, end_step__isnull=True) ended_major_office = None - heir_id = None + heir = None if titles_to_end.exists(): for title in titles_to_end: title.end_step = step @@ -121,17 +121,25 @@ def face_mortality(game, faction, potential_action, step): "data": TitleSerializer(new_faction_leader).data } }) - + + # Create a notification and notification relations new_notification_index = Notification.objects.filter(step__phase__turn__game=game).order_by('-index')[0].index + 1 notification = Notification( index=new_notification_index, step=step, type="face_mortality", faction=senators_former_faction, - data={"senator": senator.id, "major_office": ended_major_office, "heir_senator": heir_id} + data={"senator": senator.id, "major_office": ended_major_office, "heir_senator": heir.id if heir else None} ) notification.save() + senator_notification = SenatorNotification(senator=senator, notification=notification) + senator_notification.save() + + if heir: + heir_senator_notification = SenatorNotification(senator=heir, notification=notification) + heir_senator_notification.save() + messages_to_send.append({ "operation": "create", "instance": { diff --git a/backend/rorapp/functions/rank_senators_and_factions.py b/backend/rorapp/functions/rank_senators_and_factions.py index 56a24cc6..85a83bb8 100644 --- a/backend/rorapp/functions/rank_senators_and_factions.py +++ b/backend/rorapp/functions/rank_senators_and_factions.py @@ -35,26 +35,27 @@ def rank_senators_and_factions(game_id): messages_to_send = [] # Assign rank values - rank = 0 + rank_to_assign = 0 while True: selected_senator = None # Assign the rank to a major office holder - if rank <= len(ordered_major_offices) - 1: - selected_senator = ordered_major_offices[rank].senator + if rank_to_assign <= len(ordered_major_offices) - 1: + selected_senator = ordered_major_offices[rank_to_assign].senator # Assign the rank to the first remaining senator else: selected_senator = senators.first() if selected_senator is None: break + + senators = senators.exclude(id=selected_senator.id) # Update senator's rank only if it's changed - if selected_senator.rank != rank: - selected_senator.rank = rank + if selected_senator.rank != rank_to_assign: + selected_senator.rank = rank_to_assign selected_senator.save() - senators = senators.exclude(id=selected_senator.id) messages_to_send.append({ "operation": "update", @@ -64,7 +65,7 @@ def rank_senators_and_factions(game_id): } }) - rank += 1 + rank_to_assign += 1 # Get unaligned and dead senators unaligned_senators = Senator.objects.filter(game=game_id, alive=True, faction__isnull=True) diff --git a/backend/rorapp/functions/select_faction_leader.py b/backend/rorapp/functions/select_faction_leader.py index d7a3a1b5..d5731105 100644 --- a/backend/rorapp/functions/select_faction_leader.py +++ b/backend/rorapp/functions/select_faction_leader.py @@ -1,7 +1,7 @@ from rest_framework.response import Response from channels.layers import get_channel_layer from asgiref.sync import async_to_sync -from rorapp.models import Faction, PotentialAction, CompletedAction, Step, Senator, Title, Phase, Turn, Notification +from rorapp.models import Faction, PotentialAction, CompletedAction, Step, Senator, Title, Phase, Turn, Notification, SenatorNotification from rorapp.serializers import NotificationSerializer, PotentialActionSerializer, StepSerializer, TitleSerializer, PhaseSerializer @@ -61,6 +61,7 @@ def select_faction_leader(game, faction, potential_action, step, data): } }) + # Create a notification and notification relations all_notifications = Notification.objects.filter(step__phase__turn__game=game).order_by('-index') new_notification_index = 0 if all_notifications.count() > 0: @@ -75,6 +76,13 @@ def select_faction_leader(game, faction, potential_action, step, data): ) notification.save() + senator_notification = SenatorNotification(senator=senator, notification=notification) + senator_notification.save() + + if previous_senator_id: + previous_senator_notification = SenatorNotification(senator=previous_senator_id, notification=notification) + previous_senator_notification.save() + messages_to_send.append({ "operation": "create", "instance": { diff --git a/backend/rorapp/migrations/0033_senatornotification.py b/backend/rorapp/migrations/0033_senatornotification.py new file mode 100644 index 00000000..a0babc85 --- /dev/null +++ b/backend/rorapp/migrations/0033_senatornotification.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.2 on 2023-09-24 09:24 + +from django.db import migrations, models +import django.db.models.deletion + + +# Create relationships between senators and notifications +def create_senator_notifications(apps, schema_editor): + Notification = apps.get_model('rorapp', 'Notification') + Senator = apps.get_model('rorapp', 'Senator') + SenatorNotification = apps.get_model('rorapp', 'SenatorNotification') + + notifications = Notification.objects.all() + + # For each notification create some relations + for notification in notifications: + + # Look at notification data and create a list of keys that end in "senator" + senator_keys = [key for key in notification.data.keys() if key.endswith('senator')] + + for senator_key in senator_keys: + + # Create a SenatorNotification for each senator ID in the notification data + senator_id = notification.data[senator_key] + if senator_id: + senator = Senator.objects.get(id=senator_id) + SenatorNotification.objects.create(senator=senator, notification=notification) + + +class Migration(migrations.Migration): + + dependencies = [ + ('rorapp', '0032_change_senator_notification_data'), + ] + + operations = [ + migrations.CreateModel( + name='SenatorNotification', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('notification', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rorapp.notification')), + ('senator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rorapp.senator')), + ], + ), + migrations.RunPython(create_senator_notifications, migrations.RunPython.noop), + ] diff --git a/backend/rorapp/models/__init__.py b/backend/rorapp/models/__init__.py index 6fbb46bb..0fe2bbb3 100644 --- a/backend/rorapp/models/__init__.py +++ b/backend/rorapp/models/__init__.py @@ -7,6 +7,7 @@ from .phase import Phase from .potential_action import PotentialAction from .senator import Senator +from .senator_notification import SenatorNotification from .step import Step from .title import Title from .turn import Turn diff --git a/backend/rorapp/models/notification.py b/backend/rorapp/models/notification.py index 07d599d0..f99cdff3 100644 --- a/backend/rorapp/models/notification.py +++ b/backend/rorapp/models/notification.py @@ -10,3 +10,7 @@ class Notification(models.Model): type = models.CharField(max_length=50) faction = models.ForeignKey(Faction, on_delete=models.CASCADE, blank=True, null=True) data = models.JSONField(blank=True, null=True) + + # String representation of the notification, used in admin site + def __str__(self): + return f'Notification {self.index} in {self.step.phase.turn.game}: {self.type}' diff --git a/backend/rorapp/models/senator.py b/backend/rorapp/models/senator.py index 7a0db0df..8fa7c9cc 100644 --- a/backend/rorapp/models/senator.py +++ b/backend/rorapp/models/senator.py @@ -30,4 +30,4 @@ def votes(self): # String representation of the senator, used in admin site def __str__(self): - return f'{self.name} in {self.game}' + return f'{self.name}{f" ({self.generation}) " if self.generation > 1 else " "}in {self.game}' diff --git a/backend/rorapp/models/senator_notification.py b/backend/rorapp/models/senator_notification.py new file mode 100644 index 00000000..77f57190 --- /dev/null +++ b/backend/rorapp/models/senator_notification.py @@ -0,0 +1,10 @@ +from django.db import models +from rorapp.models.senator import Senator +from rorapp.models.notification import Notification + + +# Model for representing relationships between senators and notifications + +class SenatorNotification(models.Model): + senator = models.ForeignKey(Senator, on_delete=models.CASCADE) + notification = models.ForeignKey(Notification, on_delete=models.CASCADE) diff --git a/frontend/components/notifications/Notification_FaceMortality.tsx b/frontend/components/notifications/Notification_FaceMortality.tsx index bf67d955..cbb569d4 100644 --- a/frontend/components/notifications/Notification_FaceMortality.tsx +++ b/frontend/components/notifications/Notification_FaceMortality.tsx @@ -31,7 +31,7 @@ const FaceMortalityNotification = (props: FaceMortalityNotificationProps) => { useEffect(() => { if (props.notification.data) { setSenator(allSenators.byId[props.notification.data.senator] ?? null) - setHeir(allSenators.byId[props.notification.data.heir] ?? null) + setHeir(allSenators.byId[props.notification.data.heir_senator] ?? null) } }, [props.notification, allSenators, setFaction, setHeir]) From 116bab703b69a4ea6f158911ef3b6ffe1ab29c2b Mon Sep 17 00:00:00 2001 From: Logan Davidson Date: Sun, 24 Sep 2023 12:16:44 +0100 Subject: [PATCH 03/21] Rename Notification to ActionLog in the backend Rename `Notification` to `ActionLog` in the backend, because it will be used for logging as well as for notifications. The thing called a "notification" in the frontend has not been renamed because it's still being used only for notifications, for now. --- backend/rorapp/admin/__init__.py | 4 +- backend/rorapp/admin/action_log.py | 8 ++++ backend/rorapp/admin/notification.py | 8 ---- backend/rorapp/admin/senator_action_log.py | 8 ++++ backend/rorapp/admin/senator_notification.py | 8 ---- backend/rorapp/functions/face_mortality.py | 24 ++++++------ .../rorapp/functions/select_faction_leader.py | 32 ++++++++-------- ..._rename_notification_actionlog_and_more.py | 37 +++++++++++++++++++ backend/rorapp/models/__init__.py | 4 +- .../models/{notification.py => action_log.py} | 8 ++-- backend/rorapp/models/senator_action_log.py | 10 +++++ backend/rorapp/models/senator_notification.py | 10 ----- backend/rorapp/serializers/__init__.py | 2 +- backend/rorapp/serializers/action_log.py | 10 +++++ backend/rorapp/serializers/notification.py | 10 ----- backend/rorapp/urls.py | 2 +- backend/rorapp/views/__init__.py | 2 +- .../views/{notification.py => action_log.py} | 12 +++--- frontend/components/GamePage.tsx | 2 +- 19 files changed, 119 insertions(+), 82 deletions(-) create mode 100644 backend/rorapp/admin/action_log.py delete mode 100644 backend/rorapp/admin/notification.py create mode 100644 backend/rorapp/admin/senator_action_log.py delete mode 100644 backend/rorapp/admin/senator_notification.py create mode 100644 backend/rorapp/migrations/0034_senatoractionlog_rename_notification_actionlog_and_more.py rename backend/rorapp/models/{notification.py => action_log.py} (64%) create mode 100644 backend/rorapp/models/senator_action_log.py delete mode 100644 backend/rorapp/models/senator_notification.py create mode 100644 backend/rorapp/serializers/action_log.py delete mode 100644 backend/rorapp/serializers/notification.py rename backend/rorapp/views/{notification.py => action_log.py} (84%) diff --git a/backend/rorapp/admin/__init__.py b/backend/rorapp/admin/__init__.py index 5a1d64db..8f63f293 100644 --- a/backend/rorapp/admin/__init__.py +++ b/backend/rorapp/admin/__init__.py @@ -2,12 +2,12 @@ from .completed_action import CompletedActionAdmin from .faction import FactionAdmin from .game import GameAdmin -from .notification import NotificationAdmin +from .action_log import ActionLogAdmin from .player import PlayerAdmin from .phase import PhaseAdmin from .potential_action import PotentialActionAdmin from .senator import SenatorAdmin -from .senator_notification import SenatorNotificationAdmin +from .senator_action_log import SenatorActionLogAdmin from .step import StepAdmin from .title import TitleAdmin from .turn import TurnAdmin diff --git a/backend/rorapp/admin/action_log.py b/backend/rorapp/admin/action_log.py new file mode 100644 index 00000000..8b3b3e19 --- /dev/null +++ b/backend/rorapp/admin/action_log.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from rorapp.models import ActionLog + + +# Admin configuration for action_logs +@admin.register(ActionLog) +class ActionLogAdmin(admin.ModelAdmin): + list_display = ('id', 'index', 'step', 'type', 'faction') diff --git a/backend/rorapp/admin/notification.py b/backend/rorapp/admin/notification.py deleted file mode 100644 index 0ddbef0c..00000000 --- a/backend/rorapp/admin/notification.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.contrib import admin -from rorapp.models import Notification - - -# Admin configuration for notifications -@admin.register(Notification) -class NotificationAdmin(admin.ModelAdmin): - list_display = ('id', 'index', 'step', 'type', 'faction') diff --git a/backend/rorapp/admin/senator_action_log.py b/backend/rorapp/admin/senator_action_log.py new file mode 100644 index 00000000..121663a3 --- /dev/null +++ b/backend/rorapp/admin/senator_action_log.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from rorapp.models import SenatorActionLog + + +# Admin configuration for action_logs +@admin.register(SenatorActionLog) +class SenatorActionLogAdmin(admin.ModelAdmin): + list_display = ('id', 'senator', 'action_log') diff --git a/backend/rorapp/admin/senator_notification.py b/backend/rorapp/admin/senator_notification.py deleted file mode 100644 index e24a7293..00000000 --- a/backend/rorapp/admin/senator_notification.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.contrib import admin -from rorapp.models import SenatorNotification - - -# Admin configuration for notifications -@admin.register(SenatorNotification) -class SenatorNotificationAdmin(admin.ModelAdmin): - list_display = ('id', 'senator', 'notification') diff --git a/backend/rorapp/functions/face_mortality.py b/backend/rorapp/functions/face_mortality.py index 83a19537..c4627c83 100644 --- a/backend/rorapp/functions/face_mortality.py +++ b/backend/rorapp/functions/face_mortality.py @@ -6,8 +6,8 @@ from asgiref.sync import async_to_sync from rorapp.functions.draw_mortality_chits import draw_mortality_chits from rorapp.functions.rank_senators_and_factions import rank_senators_and_factions -from rorapp.models import Faction, PotentialAction, CompletedAction, Step, Senator, Title, Phase, Turn, Notification, SenatorNotification -from rorapp.serializers import NotificationSerializer, PotentialActionSerializer, StepSerializer, TitleSerializer, PhaseSerializer, TurnSerializer, SenatorSerializer +from rorapp.models import Faction, PotentialAction, CompletedAction, Step, Senator, Title, Phase, Turn, ActionLog, SenatorActionLog +from rorapp.serializers import ActionLogSerializer, PotentialActionSerializer, StepSerializer, TitleSerializer, PhaseSerializer, TurnSerializer, SenatorSerializer def face_mortality(game, faction, potential_action, step): @@ -122,29 +122,29 @@ def face_mortality(game, faction, potential_action, step): } }) - # Create a notification and notification relations - new_notification_index = Notification.objects.filter(step__phase__turn__game=game).order_by('-index')[0].index + 1 - notification = Notification( - index=new_notification_index, + # Create a action_log and action_log relations + new_action_log_index = ActionLog.objects.filter(step__phase__turn__game=game).order_by('-index')[0].index + 1 + action_log = ActionLog( + index=new_action_log_index, step=step, type="face_mortality", faction=senators_former_faction, data={"senator": senator.id, "major_office": ended_major_office, "heir_senator": heir.id if heir else None} ) - notification.save() + action_log.save() - senator_notification = SenatorNotification(senator=senator, notification=notification) - senator_notification.save() + senator_action_log = SenatorActionLog(senator=senator, action_log=action_log) + senator_action_log.save() if heir: - heir_senator_notification = SenatorNotification(senator=heir, notification=notification) - heir_senator_notification.save() + heir_senator_action_log = SenatorActionLog(senator=heir, action_log=action_log) + heir_senator_action_log.save() messages_to_send.append({ "operation": "create", "instance": { "class": "notification", - "data": NotificationSerializer(notification).data + "data": ActionLogSerializer(action_log).data } }) diff --git a/backend/rorapp/functions/select_faction_leader.py b/backend/rorapp/functions/select_faction_leader.py index d5731105..c211aff5 100644 --- a/backend/rorapp/functions/select_faction_leader.py +++ b/backend/rorapp/functions/select_faction_leader.py @@ -1,8 +1,8 @@ from rest_framework.response import Response from channels.layers import get_channel_layer from asgiref.sync import async_to_sync -from rorapp.models import Faction, PotentialAction, CompletedAction, Step, Senator, Title, Phase, Turn, Notification, SenatorNotification -from rorapp.serializers import NotificationSerializer, PotentialActionSerializer, StepSerializer, TitleSerializer, PhaseSerializer +from rorapp.models import Faction, PotentialAction, CompletedAction, Step, Senator, Title, Phase, Turn, ActionLog, SenatorActionLog +from rorapp.serializers import ActionLogSerializer, PotentialActionSerializer, StepSerializer, TitleSerializer, PhaseSerializer def select_faction_leader(game, faction, potential_action, step, data): @@ -61,33 +61,33 @@ def select_faction_leader(game, faction, potential_action, step, data): } }) - # Create a notification and notification relations - all_notifications = Notification.objects.filter(step__phase__turn__game=game).order_by('-index') - new_notification_index = 0 - if all_notifications.count() > 0: - latest_notification = all_notifications[0] - new_notification_index = latest_notification.index + 1 - notification = Notification( - index=new_notification_index, + # Create a action_log and action_log relations + all_action_logs = ActionLog.objects.filter(step__phase__turn__game=game).order_by('-index') + new_action_log_index = 0 + if all_action_logs.count() > 0: + latest_action_log = all_action_logs[0] + new_action_log_index = latest_action_log.index + 1 + action_log = ActionLog( + index=new_action_log_index, step=step, type="select_faction_leader", faction=faction, data={"senator": senator.id, "previous_senator": previous_senator_id} ) - notification.save() + action_log.save() - senator_notification = SenatorNotification(senator=senator, notification=notification) - senator_notification.save() + senator_action_log = SenatorActionLog(senator=senator, action_log=action_log) + senator_action_log.save() if previous_senator_id: - previous_senator_notification = SenatorNotification(senator=previous_senator_id, notification=notification) - previous_senator_notification.save() + previous_senator_action_log = SenatorActionLog(senator=previous_senator_id, action_log=action_log) + previous_senator_action_log.save() messages_to_send.append({ "operation": "create", "instance": { "class": "notification", - "data": NotificationSerializer(notification).data + "data": ActionLogSerializer(action_log).data } }) diff --git a/backend/rorapp/migrations/0034_senatoractionlog_rename_notification_actionlog_and_more.py b/backend/rorapp/migrations/0034_senatoractionlog_rename_notification_actionlog_and_more.py new file mode 100644 index 00000000..7f039dc7 --- /dev/null +++ b/backend/rorapp/migrations/0034_senatoractionlog_rename_notification_actionlog_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.2 on 2023-09-24 11:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('rorapp', '0033_senatornotification'), + ] + + operations = [ + migrations.CreateModel( + name='SenatorActionLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.RenameModel( + old_name='Notification', + new_name='ActionLog', + ), + migrations.DeleteModel( + name='SenatorNotification', + ), + migrations.AddField( + model_name='senatoractionlog', + name='action_log', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rorapp.actionlog'), + ), + migrations.AddField( + model_name='senatoractionlog', + name='senator', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rorapp.senator'), + ), + ] diff --git a/backend/rorapp/models/__init__.py b/backend/rorapp/models/__init__.py index 0fe2bbb3..b9de2452 100644 --- a/backend/rorapp/models/__init__.py +++ b/backend/rorapp/models/__init__.py @@ -2,12 +2,12 @@ from .completed_action import CompletedAction from .faction import Faction from .game import Game -from .notification import Notification +from .action_log import ActionLog from .player import Player from .phase import Phase from .potential_action import PotentialAction from .senator import Senator -from .senator_notification import SenatorNotification +from .senator_action_log import SenatorActionLog from .step import Step from .title import Title from .turn import Turn diff --git a/backend/rorapp/models/notification.py b/backend/rorapp/models/action_log.py similarity index 64% rename from backend/rorapp/models/notification.py rename to backend/rorapp/models/action_log.py index f99cdff3..b2091173 100644 --- a/backend/rorapp/models/notification.py +++ b/backend/rorapp/models/action_log.py @@ -3,14 +3,14 @@ from rorapp.models.faction import Faction -# Model for representing notifications -class Notification(models.Model): +# Model for representing action_logs +class ActionLog(models.Model): index = models.PositiveIntegerField() step = models.ForeignKey(Step, on_delete=models.CASCADE) type = models.CharField(max_length=50) faction = models.ForeignKey(Faction, on_delete=models.CASCADE, blank=True, null=True) data = models.JSONField(blank=True, null=True) - # String representation of the notification, used in admin site + # String representation of the action_log, used in admin site def __str__(self): - return f'Notification {self.index} in {self.step.phase.turn.game}: {self.type}' + return f'ActionLog {self.index} in {self.step.phase.turn.game}: {self.type}' diff --git a/backend/rorapp/models/senator_action_log.py b/backend/rorapp/models/senator_action_log.py new file mode 100644 index 00000000..a5549ecd --- /dev/null +++ b/backend/rorapp/models/senator_action_log.py @@ -0,0 +1,10 @@ +from django.db import models +from rorapp.models.senator import Senator +from rorapp.models.action_log import ActionLog + + +# Model for representing relationships between senators and action_logs + +class SenatorActionLog(models.Model): + senator = models.ForeignKey(Senator, on_delete=models.CASCADE) + action_log = models.ForeignKey(ActionLog, on_delete=models.CASCADE) diff --git a/backend/rorapp/models/senator_notification.py b/backend/rorapp/models/senator_notification.py deleted file mode 100644 index 77f57190..00000000 --- a/backend/rorapp/models/senator_notification.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.db import models -from rorapp.models.senator import Senator -from rorapp.models.notification import Notification - - -# Model for representing relationships between senators and notifications - -class SenatorNotification(models.Model): - senator = models.ForeignKey(Senator, on_delete=models.CASCADE) - notification = models.ForeignKey(Notification, on_delete=models.CASCADE) diff --git a/backend/rorapp/serializers/__init__.py b/backend/rorapp/serializers/__init__.py index 2353ad7f..24ca8ffa 100644 --- a/backend/rorapp/serializers/__init__.py +++ b/backend/rorapp/serializers/__init__.py @@ -1,7 +1,7 @@ # Package used to group the serializer scripts from .faction import FactionSerializer from .game import GameSerializer, GameDetailSerializer, GameCreateSerializer, GameUpdateSerializer -from .notification import NotificationSerializer +from .action_log import ActionLogSerializer from .player import PlayerSerializer, PlayerDetailSerializer, PlayerCreateSerializer from .phase import PhaseSerializer from .potential_action import PotentialActionSerializer diff --git a/backend/rorapp/serializers/action_log.py b/backend/rorapp/serializers/action_log.py new file mode 100644 index 00000000..15685b2d --- /dev/null +++ b/backend/rorapp/serializers/action_log.py @@ -0,0 +1,10 @@ +from rest_framework import serializers +from rorapp.models import ActionLog + + +# Serializer used to read action_logs +class ActionLogSerializer(serializers.ModelSerializer): + + class Meta: + model = ActionLog + fields = ('id', 'index', 'step', 'type', 'faction', 'data') diff --git a/backend/rorapp/serializers/notification.py b/backend/rorapp/serializers/notification.py deleted file mode 100644 index bfd9750f..00000000 --- a/backend/rorapp/serializers/notification.py +++ /dev/null @@ -1,10 +0,0 @@ -from rest_framework import serializers -from rorapp.models import Notification - - -# Serializer used to read notifications -class NotificationSerializer(serializers.ModelSerializer): - - class Meta: - model = Notification - fields = ('id', 'index', 'step', 'type', 'faction', 'data') diff --git a/backend/rorapp/urls.py b/backend/rorapp/urls.py index cee05424..b9230880 100644 --- a/backend/rorapp/urls.py +++ b/backend/rorapp/urls.py @@ -7,7 +7,7 @@ router.register('factions', views.FactionViewSet, basename='faction') router.register('games', views.GameViewSet, basename='game') router.register('players', views.PlayerViewSet, basename='game-player') -router.register('notifications', views.NotificationViewSet, basename='notification') +router.register('action-logs', views.ActionLogViewSet, basename='action-log') router.register('phases', views.PhaseViewSet, basename='phase') router.register('potential-actions', views.PotentialActionViewSet, basename='potential-action') router.register('senators', views.SenatorViewSet, basename='senator') diff --git a/backend/rorapp/views/__init__.py b/backend/rorapp/views/__init__.py index cdbe74ee..00b00113 100644 --- a/backend/rorapp/views/__init__.py +++ b/backend/rorapp/views/__init__.py @@ -2,7 +2,7 @@ from .faction import FactionViewSet from .game import GameViewSet from .index import index -from .notification import NotificationViewSet +from .action_log import ActionLogViewSet from .player import PlayerViewSet from .phase import PhaseViewSet from .potential_action import PotentialActionViewSet diff --git a/backend/rorapp/views/notification.py b/backend/rorapp/views/action_log.py similarity index 84% rename from backend/rorapp/views/notification.py rename to backend/rorapp/views/action_log.py index a80add0d..cfe3d593 100644 --- a/backend/rorapp/views/notification.py +++ b/backend/rorapp/views/action_log.py @@ -1,8 +1,8 @@ from django.db.models import Max from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated -from rorapp.models import Notification -from rorapp.serializers import NotificationSerializer +from rorapp.models import ActionLog +from rorapp.serializers import ActionLogSerializer def normalize_index(index, queryset): @@ -23,16 +23,16 @@ def normalize_index(index, queryset): return index -class NotificationViewSet(viewsets.ReadOnlyModelViewSet): +class ActionLogViewSet(viewsets.ReadOnlyModelViewSet): """ - Read notifications. + Read action_logs. """ permission_classes = [IsAuthenticated] - serializer_class = NotificationSerializer + serializer_class = ActionLogSerializer def get_queryset(self): - queryset = Notification.objects.all() + queryset = ActionLog.objects.all() # Filter against a `game` query parameter in the URL game_id = self.request.query_params.get('game', None) diff --git a/frontend/components/GamePage.tsx b/frontend/components/GamePage.tsx index eb733a1f..8b94f0f5 100644 --- a/frontend/components/GamePage.tsx +++ b/frontend/components/GamePage.tsx @@ -204,7 +204,7 @@ const GamePage = (props: GamePageProps) => { const minIndex = -10 // Fetch the last 10 notifications const maxIndex = -1 - const response = await request('GET', `notifications/?game=${props.gameId}&min_index=${minIndex}&max_index=${maxIndex}`, accessToken, refreshToken, setAccessToken, setRefreshToken, setUser) + const response = await request('GET', `action-logs/?game=${props.gameId}&min_index=${minIndex}&max_index=${maxIndex}`, accessToken, refreshToken, setAccessToken, setRefreshToken, setUser) if (response?.status === 200) { const deserializedInstances = deserializeToInstances(Notification, response.data) setNotifications((notifications) => { From c01bfcc8075261583ed083250816601f349363a8 Mon Sep 17 00:00:00 2001 From: Logan Davidson Date: Mon, 25 Sep 2023 21:18:18 +0100 Subject: [PATCH 04/21] Allow action logs to be filtered by senator Update the action log view so action logs can be filtered by senator, using the senator-action log relation. Fix migration script 0034 by stopping it from deleting all `SenatorActionLog` data. Update existing model `__str__` functions to include instance ID, making the admin site easier to use. --- .../migrations/0033_senatornotification.py | 1 + ..._rename_notification_actionlog_and_more.py | 22 +++++-------------- backend/rorapp/models/action_log.py | 2 +- backend/rorapp/models/faction.py | 2 +- backend/rorapp/models/game.py | 2 +- backend/rorapp/models/phase.py | 2 +- backend/rorapp/models/player.py | 2 +- backend/rorapp/models/senator.py | 2 +- backend/rorapp/models/senator_action_log.py | 1 - backend/rorapp/models/step.py | 2 +- backend/rorapp/models/title.py | 2 +- backend/rorapp/models/turn.py | 2 +- backend/rorapp/views/action_log.py | 21 +++++++++++++++--- 13 files changed, 34 insertions(+), 29 deletions(-) diff --git a/backend/rorapp/migrations/0033_senatornotification.py b/backend/rorapp/migrations/0033_senatornotification.py index a0babc85..1a035195 100644 --- a/backend/rorapp/migrations/0033_senatornotification.py +++ b/backend/rorapp/migrations/0033_senatornotification.py @@ -22,6 +22,7 @@ def create_senator_notifications(apps, schema_editor): # Create a SenatorNotification for each senator ID in the notification data senator_id = notification.data[senator_key] + if senator_id: senator = Senator.objects.get(id=senator_id) SenatorNotification.objects.create(senator=senator, notification=notification) diff --git a/backend/rorapp/migrations/0034_senatoractionlog_rename_notification_actionlog_and_more.py b/backend/rorapp/migrations/0034_senatoractionlog_rename_notification_actionlog_and_more.py index 7f039dc7..21d44656 100644 --- a/backend/rorapp/migrations/0034_senatoractionlog_rename_notification_actionlog_and_more.py +++ b/backend/rorapp/migrations/0034_senatoractionlog_rename_notification_actionlog_and_more.py @@ -11,27 +11,17 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name='SenatorActionLog', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ], + migrations.RenameModel( + old_name='SenatorNotification', + new_name='SenatorActionLog', ), migrations.RenameModel( old_name='Notification', new_name='ActionLog', ), - migrations.DeleteModel( - name='SenatorNotification', - ), - migrations.AddField( - model_name='senatoractionlog', - name='action_log', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rorapp.actionlog'), - ), - migrations.AddField( + migrations.RenameField( model_name='senatoractionlog', - name='senator', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rorapp.senator'), + old_name='notification', + new_name='action_log', ), ] diff --git a/backend/rorapp/models/action_log.py b/backend/rorapp/models/action_log.py index b2091173..08cd496d 100644 --- a/backend/rorapp/models/action_log.py +++ b/backend/rorapp/models/action_log.py @@ -13,4 +13,4 @@ class ActionLog(models.Model): # String representation of the action_log, used in admin site def __str__(self): - return f'ActionLog {self.index} in {self.step.phase.turn.game}: {self.type}' + return f'{self.id}: {self.index} in {self.step.phase.turn.game}: {self.type}' diff --git a/backend/rorapp/models/faction.py b/backend/rorapp/models/faction.py index e62d718b..e6aff434 100644 --- a/backend/rorapp/models/faction.py +++ b/backend/rorapp/models/faction.py @@ -12,4 +12,4 @@ class Faction(models.Model): # String representation of the faction, used in admin site def __str__(self): - return f'Faction {self.position} in {self.game}' + return f'{self.id}: faction {self.position} in {self.game}' diff --git a/backend/rorapp/models/game.py b/backend/rorapp/models/game.py index e10f1c9c..7d8d9577 100644 --- a/backend/rorapp/models/game.py +++ b/backend/rorapp/models/game.py @@ -13,4 +13,4 @@ class Game(models.Model): # String representation of the game, used in admin site def __str__(self): - return self.name + return f"{self.id}: {self.name}" diff --git a/backend/rorapp/models/phase.py b/backend/rorapp/models/phase.py index bab8600e..d0bf488a 100644 --- a/backend/rorapp/models/phase.py +++ b/backend/rorapp/models/phase.py @@ -10,4 +10,4 @@ class Phase(models.Model): # String representation of the phase, used in admin site def __str__(self): - return f'{self.name} phase of turn {self.turn.index} in {self.turn.game}' + return f'{self.id}: {self.name} phase of turn {self.turn.index} in {self.turn.game}' diff --git a/backend/rorapp/models/player.py b/backend/rorapp/models/player.py index 16eef9f0..58311916 100644 --- a/backend/rorapp/models/player.py +++ b/backend/rorapp/models/player.py @@ -12,4 +12,4 @@ class Player(models.Model): # String representation of the game player, used in admin site def __str__(self): - return f'{self.user} in {self.game}' + return f'{self.id}: {self.user} in {self.game}' diff --git a/backend/rorapp/models/senator.py b/backend/rorapp/models/senator.py index 8fa7c9cc..2b076865 100644 --- a/backend/rorapp/models/senator.py +++ b/backend/rorapp/models/senator.py @@ -30,4 +30,4 @@ def votes(self): # String representation of the senator, used in admin site def __str__(self): - return f'{self.name}{f" ({self.generation}) " if self.generation > 1 else " "}in {self.game}' + return f'{self.id}: {self.name}{f" ({self.generation}) " if self.generation > 1 else " "}in {self.game}' diff --git a/backend/rorapp/models/senator_action_log.py b/backend/rorapp/models/senator_action_log.py index a5549ecd..6ffa06db 100644 --- a/backend/rorapp/models/senator_action_log.py +++ b/backend/rorapp/models/senator_action_log.py @@ -4,7 +4,6 @@ # Model for representing relationships between senators and action_logs - class SenatorActionLog(models.Model): senator = models.ForeignKey(Senator, on_delete=models.CASCADE) action_log = models.ForeignKey(ActionLog, on_delete=models.CASCADE) diff --git a/backend/rorapp/models/step.py b/backend/rorapp/models/step.py index ad5a6df3..8e1611d9 100644 --- a/backend/rorapp/models/step.py +++ b/backend/rorapp/models/step.py @@ -9,4 +9,4 @@ class Step(models.Model): # String representation of the step, used in admin site def __str__(self): - return f'Step {self.index} in {self.phase.turn.game}' + return f'{self.id}: step {self.index} in {self.phase.turn.game}' diff --git a/backend/rorapp/models/title.py b/backend/rorapp/models/title.py index e6062e24..bf8decb2 100644 --- a/backend/rorapp/models/title.py +++ b/backend/rorapp/models/title.py @@ -13,4 +13,4 @@ class Title(models.Model): # String representation of the title, used in admin site def __str__(self): - return f'{self.name} {self.senator}' + return f'{self.id}: {self.name} {self.senator}' diff --git a/backend/rorapp/models/turn.py b/backend/rorapp/models/turn.py index 087db6dc..a34cb4f0 100644 --- a/backend/rorapp/models/turn.py +++ b/backend/rorapp/models/turn.py @@ -9,4 +9,4 @@ class Turn(models.Model): # String representation of the phase, used in admin site def __str__(self): - return f'Turn {self.index} of {self.game}' + return f'{self.id}: {self.index} of {self.game}' diff --git a/backend/rorapp/views/action_log.py b/backend/rorapp/views/action_log.py index cfe3d593..7c22e25a 100644 --- a/backend/rorapp/views/action_log.py +++ b/backend/rorapp/views/action_log.py @@ -1,7 +1,7 @@ -from django.db.models import Max +from django.db.models import Max, Exists, OuterRef from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated -from rorapp.models import ActionLog +from rorapp.models import ActionLog, SenatorActionLog from rorapp.serializers import ActionLogSerializer @@ -25,7 +25,7 @@ def normalize_index(index, queryset): class ActionLogViewSet(viewsets.ReadOnlyModelViewSet): """ - Read action_logs. + Read action logs. Optionally accepts `game`, `senator`, `min_index` and `max_index` URL parameters for filtering. """ permission_classes = [IsAuthenticated] @@ -38,6 +38,21 @@ def get_queryset(self): game_id = self.request.query_params.get('game', None) if game_id is not None: queryset = queryset.filter(step__phase__turn__game__id=game_id) + + # Filter against a `senator` query parameter in the URL + senator_id = self.request.query_params.get('senator', None) + if senator_id is not None: + # Use an annotation to check for the existence of a related SenatorActionLog + queryset = queryset.annotate( + has_senator_log=Exists( + SenatorActionLog.objects.filter( + senator_id=senator_id, + action_log=OuterRef('id') + ) + ) + ) + # Filter only where the SenatorActionLog exists + queryset = queryset.filter(has_senator_log=True) # Filter against a `min_index` query parameter in the URL min_index = self.request.query_params.get('min_index', None) From 8a321b1a3254ca6a11566d73c6d2b78d8099db84 Mon Sep 17 00:00:00 2001 From: Logan Davidson Date: Wed, 27 Sep 2023 08:30:04 +0100 Subject: [PATCH 05/21] Set maximum widths for sections Set maximum widths for the detail and progress sections. --- frontend/components/GamePage.module.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/components/GamePage.module.css b/frontend/components/GamePage.module.css index 7c6dc7bf..bbb4ee82 100644 --- a/frontend/components/GamePage.module.css +++ b/frontend/components/GamePage.module.css @@ -62,6 +62,10 @@ width: 100%; flex: 1; } + + .normalSection:not(.mainSection) { + max-width: 540px; + } .mainSection { flex: 2; From 9fa4cef2cf2b1da562bc8ee778dacc1d8fe41f7b Mon Sep 17 00:00:00 2001 From: Logan Davidson Date: Wed, 27 Sep 2023 18:42:19 +0100 Subject: [PATCH 06/21] Add senator details tab selectors --- frontend/components/GamePage.tsx | 9 +++-- .../detailSections/DetailSection_Senator.tsx | 35 ++++++++++++++----- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/frontend/components/GamePage.tsx b/frontend/components/GamePage.tsx index 8b94f0f5..ee5a17ca 100644 --- a/frontend/components/GamePage.tsx +++ b/frontend/components/GamePage.tsx @@ -263,10 +263,6 @@ const GamePage = (props: GamePageProps) => { console.log(`[Full Sync] completed in ${timeTaken}ms`) }, [refreshingToken, user, fetchGame, fetchPlayers, fetchFactions, fetchSenators, fetchTitles, fetchLatestTurn, fetchLatestPhase, fetchLatestStep, fetchPotentialActions, fetchNotifications]) - const handleMainTabChange = (event: React.SyntheticEvent, newValue: number) => { - setMainTab(newValue) - } - // Read WebSocket messages and use payloads to update state useEffect(() => { if (lastMessage?.data) { @@ -427,6 +423,10 @@ const GamePage = (props: GamePageProps) => { } }, [lastMessage, game?.id, setLatestTurn, setLatestPhase, setLatestStep, setPotentialActions, setAllTitles, setAllSenators, setNotifications]) + const handleMainTabChange = (event: React.SyntheticEvent, newValue: number) => { + setMainTab(newValue) + } + // Sign out if authentication failed on the server useEffect(() => { if (props.authFailure) { @@ -436,7 +436,6 @@ const GamePage = (props: GamePageProps) => { } }, [props.authFailure, setAccessToken, setRefreshToken, setUser]) - // === Rendering === if (!syncingGameData) { diff --git a/frontend/components/detailSections/DetailSection_Senator.tsx b/frontend/components/detailSections/DetailSection_Senator.tsx index 4e834ea3..8956ba34 100644 --- a/frontend/components/detailSections/DetailSection_Senator.tsx +++ b/frontend/components/detailSections/DetailSection_Senator.tsx @@ -1,5 +1,5 @@ import Image from 'next/image' -import { RefObject } from "react" +import { RefObject, useState } from "react" import SenatorPortrait from "@/components/SenatorPortrait" import Senator from "@/classes/Senator" @@ -18,6 +18,7 @@ import KnightsIcon from "@/images/icons/knights.svg" import VotesIcon from "@/images/icons/votes.svg" import FactionLink from '@/components/FactionLink' import Title from '@/classes/Title' +import { Box, Tab, Tabs } from '@mui/material' type FixedAttribute = { name: "military" | "oratory" | "loyalty" @@ -51,6 +52,9 @@ const SenatorDetails = (props: SenatorDetailsProps) => { const majorOffice: Title | null = senator ? allTitles.asArray.find(o => o.senator === senator.id && o.major_office == true) ?? null : null const factionLeader: boolean = senator ? allTitles.asArray.some(o => o.senator === senator.id && o.name == 'Faction Leader') : false + // UI selections + const [tab, setTab] = useState(0) + // Calculate senator portrait size. // Image size must be defined in JavaScript rather than in CSS const getPortraitSize = () => { @@ -65,6 +69,10 @@ const SenatorDetails = (props: SenatorDetailsProps) => { return 200 } } + + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setTab(newValue) + } // Fixed attribute data const fixedAttributeItems: FixedAttribute[] = senator ? [ @@ -157,14 +165,25 @@ const SenatorDetails = (props: SenatorDetailsProps) => { {majorOffice &&

Serving as {majorOffice?.name}

} -
-
- {fixedAttributeItems.map(item => getFixedAttributeRow(item))} -
-
- {variableAttributeItems.map(item => getVariableAttributeRow(item))} + + + + + + + {tab === 0 && +
+
+ {fixedAttributeItems.map(item => getFixedAttributeRow(item))} +
+
+ {variableAttributeItems.map(item => getVariableAttributeRow(item))} +
-
+ } + {tab === 1 && +
History
+ }
) } From a3a7292ab9e94a54abc3e39f825638d030a8bfa5 Mon Sep 17 00:00:00 2001 From: Logan Davidson Date: Wed, 27 Sep 2023 18:44:11 +0100 Subject: [PATCH 07/21] Rename the `Notification` class in the frontend Rename the `Notification` class in the frontend to `ActionLog`, but leave variables and state that use it as-is, because those still use the `ActionLog` items as notifications. --- frontend/classes/{Notification.ts => ActionLog.ts} | 8 ++++---- frontend/components/GamePage.tsx | 10 +++++----- frontend/components/ProgressSection.tsx | 4 ++-- frontend/components/notifications/Notification.tsx | 4 ++-- .../notifications/Notification_FaceMortality.tsx | 4 ++-- .../notifications/Notification_SelectFactionLeader.tsx | 4 ++-- 6 files changed, 17 insertions(+), 17 deletions(-) rename frontend/classes/{Notification.ts => ActionLog.ts} (75%) diff --git a/frontend/classes/Notification.ts b/frontend/classes/ActionLog.ts similarity index 75% rename from frontend/classes/Notification.ts rename to frontend/classes/ActionLog.ts index 03474be9..edbe9bd1 100644 --- a/frontend/classes/Notification.ts +++ b/frontend/classes/ActionLog.ts @@ -1,4 +1,4 @@ -interface NotificationData { +interface ActionLogData { id: number index: number step: number @@ -7,7 +7,7 @@ interface NotificationData { data: string } -class Notification { +class ActionLog { id: number index: number step: number @@ -15,7 +15,7 @@ class Notification { faction: number | null data: any - constructor(data: NotificationData) { + constructor(data: ActionLogData) { this.id = data.id this.index = data.index this.step = data.step @@ -25,4 +25,4 @@ class Notification { } } -export default Notification +export default ActionLog diff --git a/frontend/components/GamePage.tsx b/frontend/components/GamePage.tsx index ee5a17ca..a9d28ec5 100644 --- a/frontend/components/GamePage.tsx +++ b/frontend/components/GamePage.tsx @@ -29,7 +29,7 @@ import Step from '@/classes/Step' import MetaSection from '@/components/MetaSection' import PotentialAction from '@/classes/PotentialAction' import ProgressSection from '@/components/ProgressSection' -import Notification from '@/classes/Notification' +import ActionLog from '@/classes/ActionLog' import refreshAccessToken from "@/functions/tokens" const webSocketURL: string = process.env.NEXT_PUBLIC_WS_URL ?? ""; @@ -56,7 +56,7 @@ const GamePage = (props: GamePageProps) => { const [latestTurn, setLatestTurn] = useState(null) const [latestPhase, setLatestPhase] = useState(null) const [potentialActions, setPotentialActions] = useState>(new Collection()) - const [notifications, setNotifications] = useState>(new Collection()) + const [notifications, setNotifications] = useState>(new Collection()) // Set game-specific state using initial data useEffect(() => { @@ -206,7 +206,7 @@ const GamePage = (props: GamePageProps) => { const response = await request('GET', `action-logs/?game=${props.gameId}&min_index=${minIndex}&max_index=${maxIndex}`, accessToken, refreshToken, setAccessToken, setRefreshToken, setUser) if (response?.status === 200) { - const deserializedInstances = deserializeToInstances(Notification, response.data) + const deserializedInstances = deserializeToInstances(ActionLog, response.data) setNotifications((notifications) => { // Merge the new notifications with the existing ones // Loop over each new notification and add it to the collection if it doesn't already exist @@ -218,7 +218,7 @@ const GamePage = (props: GamePageProps) => { return notifications }) } else { - setNotifications(new Collection()) + setNotifications(new Collection()) } }, [props.gameId, accessToken, refreshToken, setAccessToken, setRefreshToken, setUser]) @@ -403,7 +403,7 @@ const GamePage = (props: GamePageProps) => { // Add a notification if (message?.operation === "create") { - const newInstance = deserializeToInstance(Notification, message.instance.data) + const newInstance = deserializeToInstance(ActionLog, message.instance.data) // Before updating state, ensure that this instance has not already been added if (newInstance) { setNotifications( diff --git a/frontend/components/ProgressSection.tsx b/frontend/components/ProgressSection.tsx index d7327ca6..520eaf78 100644 --- a/frontend/components/ProgressSection.tsx +++ b/frontend/components/ProgressSection.tsx @@ -12,14 +12,14 @@ import { useAuthContext } from "@/contexts/AuthContext" import ActionDialog from "@/components/actionDialogs/ActionDialog" import ActionsType from "@/types/actions" import Faction from "@/classes/Faction" -import Notification from "@/classes/Notification" +import ActionLog from "@/classes/ActionLog" import NotificationContainer from "@/components/notifications/Notification" const typedActions: ActionsType = Actions interface ProgressSectionProps { allPotentialActions: Collection - notifications: Collection + notifications: Collection } // Progress section showing who players are waiting for diff --git a/frontend/components/notifications/Notification.tsx b/frontend/components/notifications/Notification.tsx index 21282dd0..97eab995 100644 --- a/frontend/components/notifications/Notification.tsx +++ b/frontend/components/notifications/Notification.tsx @@ -1,9 +1,9 @@ -import Notification from "@/classes/Notification" +import ActionLog from "@/classes/ActionLog" import SelectFactionLeaderNotification from "./Notification_SelectFactionLeader" import FaceMortalityNotification from "./Notification_FaceMortality" interface NotificationItemProps { - notification: Notification + notification: ActionLog } const notifications: { [key: string]: React.ComponentType } = { diff --git a/frontend/components/notifications/Notification_FaceMortality.tsx b/frontend/components/notifications/Notification_FaceMortality.tsx index cbb569d4..360c2298 100644 --- a/frontend/components/notifications/Notification_FaceMortality.tsx +++ b/frontend/components/notifications/Notification_FaceMortality.tsx @@ -1,7 +1,7 @@ import Image from 'next/image' import { Alert } from "@mui/material" -import Notification from "@/classes/Notification" +import ActionLog from "@/classes/ActionLog" import { useGameContext } from "@/contexts/GameContext" import { useEffect, useState } from "react" import Faction from "@/classes/Faction" @@ -12,7 +12,7 @@ import SenatorLink from "@/components/SenatorLink" import FactionLink from '@/components/FactionLink' interface FaceMortalityNotificationProps { - notification: Notification + notification: ActionLog } const FaceMortalityNotification = (props: FaceMortalityNotificationProps) => { diff --git a/frontend/components/notifications/Notification_SelectFactionLeader.tsx b/frontend/components/notifications/Notification_SelectFactionLeader.tsx index 04516838..a8694739 100644 --- a/frontend/components/notifications/Notification_SelectFactionLeader.tsx +++ b/frontend/components/notifications/Notification_SelectFactionLeader.tsx @@ -1,5 +1,5 @@ import { Alert } from "@mui/material" -import Notification from "@/classes/Notification" +import ActionLog from "@/classes/ActionLog" import FactionIcon from "@/components/FactionIcon" import { useGameContext } from "@/contexts/GameContext" import { useEffect, useState } from "react" @@ -10,7 +10,7 @@ import SenatorLink from "@/components/SenatorLink" import FactionLink from '@/components/FactionLink' interface SelectFactionLeaderNotificationProps { - notification: Notification + notification: ActionLog } // Notification for when a new faction leader is selected From 1d01ab72f4e68add146ff0f73d2ee86a29cc3b34 Mon Sep 17 00:00:00 2001 From: Logan Davidson Date: Thu, 28 Sep 2023 06:49:08 +0100 Subject: [PATCH 08/21] Enable frontend to build an action list Make the changes that enable the frontend to build a list of action logs, separate from the notification list, for showing action logs for each senator in the history tab. At this stage the listed actions don't look very informative. Action logs for a given senator are fetched whenever the user opens the senator detail section for that senator. They are not fetched again until the game is fully reloaded, but new log items are passed by WebSocket messages just as new notifications come in. --- backend/rorapp/functions/face_mortality.py | 29 +++++--- .../rorapp/functions/select_faction_leader.py | 29 ++++++-- backend/rorapp/serializers/__init__.py | 1 + .../rorapp/serializers/senator_action_log.py | 10 +++ backend/rorapp/urls.py | 1 + backend/rorapp/views/__init__.py | 1 + backend/rorapp/views/senator_action_log.py | 23 +++++++ frontend/classes/Senator.ts | 2 + frontend/classes/SenatorActionLog.ts | 19 ++++++ frontend/components/GamePage.tsx | 43 ++++++++++-- .../detailSections/DetailSection_Senator.tsx | 66 +++++++++++++++++-- frontend/contexts/GameContext.tsx | 12 +++- 12 files changed, 209 insertions(+), 27 deletions(-) create mode 100644 backend/rorapp/serializers/senator_action_log.py create mode 100644 backend/rorapp/views/senator_action_log.py create mode 100644 frontend/classes/SenatorActionLog.ts diff --git a/backend/rorapp/functions/face_mortality.py b/backend/rorapp/functions/face_mortality.py index c4627c83..3d936917 100644 --- a/backend/rorapp/functions/face_mortality.py +++ b/backend/rorapp/functions/face_mortality.py @@ -7,7 +7,7 @@ from rorapp.functions.draw_mortality_chits import draw_mortality_chits from rorapp.functions.rank_senators_and_factions import rank_senators_and_factions from rorapp.models import Faction, PotentialAction, CompletedAction, Step, Senator, Title, Phase, Turn, ActionLog, SenatorActionLog -from rorapp.serializers import ActionLogSerializer, PotentialActionSerializer, StepSerializer, TitleSerializer, PhaseSerializer, TurnSerializer, SenatorSerializer +from rorapp.serializers import ActionLogSerializer, PotentialActionSerializer, StepSerializer, TitleSerializer, PhaseSerializer, TurnSerializer, SenatorSerializer, SenatorActionLogSerializer def face_mortality(game, faction, potential_action, step): @@ -132,22 +132,35 @@ def face_mortality(game, faction, potential_action, step): data={"senator": senator.id, "major_office": ended_major_office, "heir_senator": heir.id if heir else None} ) action_log.save() + messages_to_send.append({ + "operation": "create", + "instance": { + "class": "action_log", + "data": ActionLogSerializer(action_log).data + } + }) senator_action_log = SenatorActionLog(senator=senator, action_log=action_log) senator_action_log.save() - - if heir: - heir_senator_action_log = SenatorActionLog(senator=heir, action_log=action_log) - heir_senator_action_log.save() - messages_to_send.append({ "operation": "create", "instance": { - "class": "notification", - "data": ActionLogSerializer(action_log).data + "class": "senator_action_log", + "data": SenatorActionLogSerializer(senator_action_log).data } }) + if heir: + heir_senator_action_log = SenatorActionLog(senator=heir, action_log=action_log) + heir_senator_action_log.save() + messages_to_send.append({ + "operation": "create", + "instance": { + "class": "senator_action_log", + "data": SenatorActionLogSerializer(heir_senator_action_log).data + } + }) + # Update senator ranks messages_to_send.extend(rank_senators_and_factions(game.id)) diff --git a/backend/rorapp/functions/select_faction_leader.py b/backend/rorapp/functions/select_faction_leader.py index c211aff5..2bc2e5c7 100644 --- a/backend/rorapp/functions/select_faction_leader.py +++ b/backend/rorapp/functions/select_faction_leader.py @@ -2,7 +2,7 @@ from channels.layers import get_channel_layer from asgiref.sync import async_to_sync from rorapp.models import Faction, PotentialAction, CompletedAction, Step, Senator, Title, Phase, Turn, ActionLog, SenatorActionLog -from rorapp.serializers import ActionLogSerializer, PotentialActionSerializer, StepSerializer, TitleSerializer, PhaseSerializer +from rorapp.serializers import ActionLogSerializer, PotentialActionSerializer, StepSerializer, TitleSerializer, PhaseSerializer, SenatorActionLogSerializer def select_faction_leader(game, faction, potential_action, step, data): @@ -76,20 +76,35 @@ def select_faction_leader(game, faction, potential_action, step, data): ) action_log.save() + messages_to_send.append({ + "operation": "create", + "instance": { + "class": "action_log", + "data": ActionLogSerializer(action_log).data + } + }) + senator_action_log = SenatorActionLog(senator=senator, action_log=action_log) senator_action_log.save() - if previous_senator_id: - previous_senator_action_log = SenatorActionLog(senator=previous_senator_id, action_log=action_log) - previous_senator_action_log.save() - messages_to_send.append({ "operation": "create", "instance": { - "class": "notification", - "data": ActionLogSerializer(action_log).data + "class": "senator_action_log", + "data": SenatorActionLogSerializer(senator_action_log).data } }) + + if previous_senator_id: + previous_senator_action_log = SenatorActionLog(senator=previous_senator_id, action_log=action_log) + previous_senator_action_log.save() + messages_to_send.append({ + "operation": "create", + "instance": { + "class": "senator_action_log", + "data": SenatorActionLogSerializer(previous_senator_action_log).data + } + }) # Delete the potential action potential_action_id = potential_action.id diff --git a/backend/rorapp/serializers/__init__.py b/backend/rorapp/serializers/__init__.py index 24ca8ffa..19c6bc93 100644 --- a/backend/rorapp/serializers/__init__.py +++ b/backend/rorapp/serializers/__init__.py @@ -6,6 +6,7 @@ from .phase import PhaseSerializer from .potential_action import PotentialActionSerializer from .senator import SenatorSerializer +from .senator_action_log import SenatorActionLogSerializer from .step import StepSerializer from .title import TitleSerializer from .token import MyTokenObtainPairSerializer, TokenObtainPairByEmailSerializer diff --git a/backend/rorapp/serializers/senator_action_log.py b/backend/rorapp/serializers/senator_action_log.py new file mode 100644 index 00000000..e27b79ca --- /dev/null +++ b/backend/rorapp/serializers/senator_action_log.py @@ -0,0 +1,10 @@ +from rest_framework import serializers +from rorapp.models import SenatorActionLog + + +# Serializer used to read senator action logs +class SenatorActionLogSerializer(serializers.ModelSerializer): + + class Meta: + model = SenatorActionLog + fields = ('id', 'senator', 'action_log') diff --git a/backend/rorapp/urls.py b/backend/rorapp/urls.py index b9230880..b7753123 100644 --- a/backend/rorapp/urls.py +++ b/backend/rorapp/urls.py @@ -11,6 +11,7 @@ router.register('phases', views.PhaseViewSet, basename='phase') router.register('potential-actions', views.PotentialActionViewSet, basename='potential-action') router.register('senators', views.SenatorViewSet, basename='senator') +router.register('senator-action-logs', views.SenatorActionLogViewSet, basename='senator-action-log') router.register('steps', views.StepViewSet, basename='step') router.register('titles', views.TitleViewSet, basename='title') router.register('turns', views.TurnViewSet, basename='turn') diff --git a/backend/rorapp/views/__init__.py b/backend/rorapp/views/__init__.py index 00b00113..67d65447 100644 --- a/backend/rorapp/views/__init__.py +++ b/backend/rorapp/views/__init__.py @@ -7,6 +7,7 @@ from .phase import PhaseViewSet from .potential_action import PotentialActionViewSet from .senator import SenatorViewSet +from .senator_action_log import SenatorActionLogViewSet from .start_game import StartGameViewSet from .step import StepViewSet from .submit_action import SubmitActionViewSet diff --git a/backend/rorapp/views/senator_action_log.py b/backend/rorapp/views/senator_action_log.py new file mode 100644 index 00000000..5b89dd2a --- /dev/null +++ b/backend/rorapp/views/senator_action_log.py @@ -0,0 +1,23 @@ +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated +from rorapp.models import SenatorActionLog +from rorapp.serializers import SenatorActionLogSerializer + + +class SenatorActionLogViewSet(viewsets.ReadOnlyModelViewSet): + """ + Read senator action logs. Optionally accepts a `senator` URL parameter for filtering. + """ + + permission_classes = [IsAuthenticated] + serializer_class = SenatorActionLogSerializer + + def get_queryset(self): + queryset = SenatorActionLog.objects.all() + + # Filter against a `senator` query parameter in the URL + senator_id = self.request.query_params.get('senator', None) + if senator_id is not None: + queryset = queryset.filter(senator__id=senator_id) + + return queryset diff --git a/frontend/classes/Senator.ts b/frontend/classes/Senator.ts index 3552d42e..7a30e945 100644 --- a/frontend/classes/Senator.ts +++ b/frontend/classes/Senator.ts @@ -37,6 +37,8 @@ class Senator { talents: number votes: number + logsFetched: boolean = false + constructor(data: SenatorData) { this.id = data.id this.name = data.name diff --git a/frontend/classes/SenatorActionLog.ts b/frontend/classes/SenatorActionLog.ts new file mode 100644 index 00000000..df6df2c6 --- /dev/null +++ b/frontend/classes/SenatorActionLog.ts @@ -0,0 +1,19 @@ +interface SenatorActionLogData { + id: number + senator: number + action_log: number +} + +class SenatorActionLog { + id: number + senator: number + action_log: number + + constructor(data: SenatorActionLogData) { + this.id = data.id + this.senator = data.senator + this.action_log = data.action_log + } +} + +export default SenatorActionLog diff --git a/frontend/components/GamePage.tsx b/frontend/components/GamePage.tsx index a9d28ec5..097ecf65 100644 --- a/frontend/components/GamePage.tsx +++ b/frontend/components/GamePage.tsx @@ -31,6 +31,7 @@ import PotentialAction from '@/classes/PotentialAction' import ProgressSection from '@/components/ProgressSection' import ActionLog from '@/classes/ActionLog' import refreshAccessToken from "@/functions/tokens" +import SenatorActionLog from '@/classes/SenatorActionLog' const webSocketURL: string = process.env.NEXT_PUBLIC_WS_URL ?? ""; @@ -52,7 +53,7 @@ const GamePage = (props: GamePageProps) => { const [refreshingToken, setRefreshingToken] = useState(false) // Game-specific state - const { game, setGame, setLatestStep, setAllPlayers, setAllFactions, setAllSenators, setAllTitles } = useGameContext() + const { game, setGame, setLatestStep, setAllPlayers, setAllFactions, setAllSenators, setAllTitles, setActionLogs, setSenatorActionLogs } = useGameContext() const [latestTurn, setLatestTurn] = useState(null) const [latestPhase, setLatestPhase] = useState(null) const [potentialActions, setPotentialActions] = useState>(new Collection()) @@ -235,14 +236,14 @@ const GamePage = (props: GamePageProps) => { // Fetch game data const requestsBatch1 = [ fetchLatestStep(), // Positional - fetchNotifications(), fetchGame(), fetchPlayers(), fetchFactions(), fetchSenators(), fetchTitles(), fetchLatestTurn(), - fetchLatestPhase() + fetchLatestPhase(), + fetchNotifications() ] const results = await Promise.all(requestsBatch1) const updatedLatestStep: Step | null = results[0] as Step | null @@ -398,10 +399,10 @@ const GamePage = (props: GamePageProps) => { } } - // Notification updates - if (message?.instance?.class === "notification") { + // Action log updates + if (message?.instance?.class === "action_log") { - // Add a notification + // Add an action log if (message?.operation === "create") { const newInstance = deserializeToInstance(ActionLog, message.instance.data) // Before updating state, ensure that this instance has not already been added @@ -415,6 +416,36 @@ const GamePage = (props: GamePageProps) => { } } ) + setActionLogs( + (instances) => { + if (instances.allIds.includes(newInstance.id)) { + return instances + } else { + return instances.add(newInstance) + } + } + ) + } + } + } + + // Senator action log updates + if (message?.instance?.class === "senator_action_log") { + + // Add a senator action log + if (message?.operation === "create") { + const newInstance = deserializeToInstance(SenatorActionLog, message.instance.data) + // Before updating state, ensure that this instance has not already been added + if (newInstance) { + setSenatorActionLogs( + (instances) => { + if (instances.allIds.includes(newInstance.id)) { + return instances + } else { + return instances.add(newInstance) + } + } + ) } } } diff --git a/frontend/components/detailSections/DetailSection_Senator.tsx b/frontend/components/detailSections/DetailSection_Senator.tsx index 8956ba34..f11ad070 100644 --- a/frontend/components/detailSections/DetailSection_Senator.tsx +++ b/frontend/components/detailSections/DetailSection_Senator.tsx @@ -1,5 +1,5 @@ import Image from 'next/image' -import { RefObject, useState } from "react" +import { RefObject, useEffect, useState } from "react" import SenatorPortrait from "@/components/SenatorPortrait" import Senator from "@/classes/Senator" @@ -19,6 +19,13 @@ import VotesIcon from "@/images/icons/votes.svg" import FactionLink from '@/components/FactionLink' import Title from '@/classes/Title' import { Box, Tab, Tabs } from '@mui/material' +import ActionLog from '@/classes/ActionLog' +import request from '@/functions/request' +import { useAuthContext } from '@/contexts/AuthContext' +import { deserializeToInstances } from '@/functions/serialize' +import Collection from '@/classes/Collection' +import SenatorActionLog from '@/classes/SenatorActionLog' +import { forEach } from 'lodash' type FixedAttribute = { name: "military" | "oratory" | "loyalty" @@ -43,18 +50,65 @@ interface SenatorDetailsProps { // Detail section content for a senator const SenatorDetails = (props: SenatorDetailsProps) => { - const { allPlayers, allFactions, allSenators, allTitles, selectedEntity } = useGameContext() + const { accessToken, refreshToken, setAccessToken, setRefreshToken, setUser } = useAuthContext() + const { allPlayers, allFactions, allSenators, setAllSenators, allTitles, selectedEntity, actionLogs, setActionLogs, senatorActionLogs, setSenatorActionLogs } = useGameContext() // Get senator-specific data const senator: Senator | null = selectedEntity?.id ? allSenators.byId[selectedEntity.id] ?? null : null const faction: Faction | null = senator?.faction ? allFactions.byId[senator.faction] ?? null : null const player: Player | null = faction?.player ? allPlayers.byId[faction.player] ?? null : null - const majorOffice: Title | null = senator ? allTitles.asArray.find(o => o.senator === senator.id && o.major_office == true) ?? null : null - const factionLeader: boolean = senator ? allTitles.asArray.some(o => o.senator === senator.id && o.name == 'Faction Leader') : false + const majorOffice: Title | null = senator ? allTitles.asArray.find(t => t.senator === senator.id && t.major_office == true) ?? null : null + const factionLeader: boolean = senator ? allTitles.asArray.some(t => t.senator === senator.id && t.name == 'Faction Leader') : false + const matchingSenatorActionLogs = senator ? senatorActionLogs.asArray.filter(l => l.senator === senator.id) : null + const matchingActionLogs = matchingSenatorActionLogs ? actionLogs.asArray.filter(a => matchingSenatorActionLogs.some(b => b.action_log === a.id)) : null // UI selections const [tab, setTab] = useState(0) + // Fetch action logs for this senator + const fetchActionLogs = async () => { + if (!senator) return + + const response = await request('GET', `action-logs/?senator=${senator.id}`, accessToken, refreshToken, setAccessToken, setRefreshToken, setUser) + if (response.status === 200) { + const deserializedInstances = deserializeToInstances(ActionLog, response.data) + setActionLogs((instances: Collection) => + new Collection(instances.asArray.concat(deserializedInstances)) + ) + } else { + setActionLogs(new Collection()) + } + } + + // Fetch senator action logs for this senator + const fetchSenatorActionLogs = async () => { + if (!senator) return + + const response = await request('GET', `senator-action-logs/?senator=${senator.id}`, accessToken, refreshToken, setAccessToken, setRefreshToken, setUser) + if (response.status === 200) { + const deserializedInstances = deserializeToInstances(SenatorActionLog, response.data) + setSenatorActionLogs((instances: Collection) => + // Loop over each instance in deserializedInstances and add it to the collection if it's not already there + new Collection(instances.asArray.concat(deserializedInstances.filter(i => !instances.asArray.some(j => j.id === i.id)))) + ) + } else { + setSenatorActionLogs(new Collection()) + } + } + + // Fetch action logs is they haven't been fetched yet + useEffect(() => { + if (!senator || senator?.logsFetched) return + + // Fetch action logs and senator action logs + fetchActionLogs() + fetchSenatorActionLogs() + + // Set logsFetched to true so that we don't fetch again + senator.logsFetched = true + setAllSenators((senators: Collection) => new Collection(senators.asArray.map(s => s.id === senator.id ? senator : s))) + }, [senator]) + // Calculate senator portrait size. // Image size must be defined in JavaScript rather than in CSS const getPortraitSize = () => { @@ -182,7 +236,9 @@ const SenatorDetails = (props: SenatorDetailsProps) => { } {tab === 1 && -
History
+
+ {matchingActionLogs && matchingActionLogs.map((actionLog: ActionLog) => {actionLog.type})} +
} ) diff --git a/frontend/contexts/GameContext.tsx b/frontend/contexts/GameContext.tsx index db578f4f..cdff528a 100644 --- a/frontend/contexts/GameContext.tsx +++ b/frontend/contexts/GameContext.tsx @@ -7,6 +7,8 @@ import Title from '@/classes/Title' import Game from '@/classes/Game' import Step from '@/classes/Step' import SelectedEntity from '@/types/selectedEntity' +import ActionLog from '@/classes/ActionLog' +import SenatorActionLog from '@/classes/SenatorActionLog' interface GameContextType { game: Game | null @@ -23,6 +25,10 @@ interface GameContextType { setAllTitles: Dispatch>> selectedEntity: SelectedEntity | null setSelectedEntity: Dispatch> + actionLogs: Collection + setActionLogs: Dispatch>> + senatorActionLogs: Collection + setSenatorActionLogs: Dispatch>> } const GameContext = createContext(null) @@ -49,6 +55,8 @@ export const GameProvider = ( props: GameProviderProps ): JSX.Element => { const [allSenators, setAllSenators] = useState>(new Collection()) const [allTitles, setAllTitles] = useState>(new Collection()) const [selectedEntity, setSelectedEntity] = useState<SelectedEntity | null>(null) + const [actionLogs, setActionLogs] = useState<Collection<ActionLog>>(new Collection<ActionLog>()) + const [senatorActionLogs, setSenatorActionLogs] = useState<Collection<SenatorActionLog>>(new Collection<SenatorActionLog>()) return ( <GameContext.Provider value={{ @@ -58,7 +66,9 @@ export const GameProvider = ( props: GameProviderProps ): JSX.Element => { allFactions, setAllFactions, allSenators, setAllSenators, allTitles, setAllTitles, - selectedEntity, setSelectedEntity + selectedEntity, setSelectedEntity, + actionLogs, setActionLogs, + senatorActionLogs, setSenatorActionLogs }}> {props.children} </GameContext.Provider> From 7d3ddaa7b409854f021c0b67aa30aabacbe86a8d Mon Sep 17 00:00:00 2001 From: Logan Davidson <iamlogandavidson@gmail.com> Date: Thu, 28 Sep 2023 07:04:34 +0100 Subject: [PATCH 09/21] Fix game setup ranking bug Fix a game setup bug where the senators and factions were not getting ranked during the initial game setup. --- backend/rorapp/views/start_game.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/rorapp/views/start_game.py b/backend/rorapp/views/start_game.py index 125b2644..a73a2e6f 100644 --- a/backend/rorapp/views/start_game.py +++ b/backend/rorapp/views/start_game.py @@ -9,6 +9,7 @@ from rest_framework.response import Response from channels.layers import get_channel_layer from asgiref.sync import async_to_sync +from rorapp.functions import rank_senators_and_factions from rorapp.models import Game, Player, Faction, Senator, Title, Turn, Phase, Step, PotentialAction from rorapp.serializers import GameDetailSerializer, TurnSerializer, PhaseSerializer, StepSerializer @@ -106,6 +107,9 @@ def start_game(self, request, game_id=None): temp_rome_consul_title = Title(name="Temporary Rome Consul", senator=senators[0], start_step=step, major_office=True) temp_rome_consul_title.save() + # Update senator ranks + rank_senators_and_factions(game.id) + # Create potential actions for faction in factions: action = PotentialAction( From 7cbba783b826e544fb27699a4b4c42613de99a60 Mon Sep 17 00:00:00 2001 From: Logan Davidson <iamlogandavidson@gmail.com> Date: Thu, 28 Sep 2023 07:13:13 +0100 Subject: [PATCH 10/21] Fix React and HTML rule violations --- frontend/components/FactionLink.tsx | 2 +- frontend/components/SenatorList.tsx | 2 +- frontend/components/SenatorListItem.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/components/FactionLink.tsx b/frontend/components/FactionLink.tsx index ed68b818..8841a67a 100644 --- a/frontend/components/FactionLink.tsx +++ b/frontend/components/FactionLink.tsx @@ -19,7 +19,7 @@ const FactionLink = (props: FactionLinkProps) => { return ( <Link component="button" onClick={handleClick} sx={{ verticalAlign: "baseline" }}> - {props.includeIcon && <span style={{ marginRight: 4 }}><FactionIcon faction={props.faction} size={17} selectable /></span>} + {props.includeIcon && <span style={{ marginRight: 4 }}><FactionIcon faction={props.faction} size={17} /></span>} {props.faction.getName()} Faction </Link> ) diff --git a/frontend/components/SenatorList.tsx b/frontend/components/SenatorList.tsx index d13eb563..1b3c4720 100644 --- a/frontend/components/SenatorList.tsx +++ b/frontend/components/SenatorList.tsx @@ -168,7 +168,7 @@ const SenatorList = (props: SenatorListProps) => { const getHeader = (header: { name: string, icon: string }) => { const titleCaseName = header.name[0].toUpperCase() + header.name.slice(1) return ( - <Tooltip title={titleCaseName} enterDelay={500} placement="top" arrow> + <Tooltip key={header.name} title={titleCaseName} enterDelay={500} placement="top" arrow> <button onClick={() => handleSortClick(header.name)} className={styles.header}> <Image src={header.icon} height={ICON_SIZE} width={ICON_SIZE} alt={`${titleCaseName} Icon`} /> {sort === header.name && <FontAwesomeIcon icon={faChevronUp} fontSize={18} />} diff --git a/frontend/components/SenatorListItem.tsx b/frontend/components/SenatorListItem.tsx index 158878e0..38c72736 100644 --- a/frontend/components/SenatorListItem.tsx +++ b/frontend/components/SenatorListItem.tsx @@ -50,7 +50,7 @@ const SenatorListItem = ({ senator, ...props }: SenatorListItemProps) => { const getAttributeItem = (item: Attribute) => { const titleCaseName = item.name[0].toUpperCase() + item.name.slice(1) return ( - <Tooltip title={titleCaseName} enterDelay={500} arrow> + <Tooltip key={item.name} title={titleCaseName} enterDelay={500} arrow> <div aria-label={titleCaseName} style={item.fixed ? { backgroundColor: skillsJSON.colors.number[item.name as FixedAttribute], boxShadow: `0px 0px 2px 2px ${skillsJSON.colors.number[item.name as FixedAttribute]}` From 1cce0e5f1f34be3b63bc8fee4b1097dfa1b204b8 Mon Sep 17 00:00:00 2001 From: Logan Davidson <iamlogandavidson@gmail.com> Date: Thu, 28 Sep 2023 08:11:10 +0100 Subject: [PATCH 11/21] Add Temporary Rome Consul notification Add a notification at the start of the game to declare the Temporary Rome Consul. Also simplify the existing notification components. --- backend/rorapp/views/start_game.py | 16 +++- .../components/notifications/Notification.tsx | 4 +- .../Notification_FaceMortality.tsx | 75 +++++++------------ .../Notification_SelectFactionLeader.tsx | 25 ++----- .../Notification_TemporaryRomeConsul.tsx | 46 ++++++++++++ 5 files changed, 99 insertions(+), 67 deletions(-) create mode 100644 frontend/components/notifications/Notification_TemporaryRomeConsul.tsx diff --git a/backend/rorapp/views/start_game.py b/backend/rorapp/views/start_game.py index a73a2e6f..efc77272 100644 --- a/backend/rorapp/views/start_game.py +++ b/backend/rorapp/views/start_game.py @@ -10,7 +10,7 @@ from channels.layers import get_channel_layer from asgiref.sync import async_to_sync from rorapp.functions import rank_senators_and_factions -from rorapp.models import Game, Player, Faction, Senator, Title, Turn, Phase, Step, PotentialAction +from rorapp.models import Game, Player, Faction, Senator, Title, Turn, Phase, Step, PotentialAction, ActionLog, SenatorActionLog from rorapp.serializers import GameDetailSerializer, TurnSerializer, PhaseSerializer, StepSerializer @@ -110,6 +110,20 @@ def start_game(self, request, game_id=None): # Update senator ranks rank_senators_and_factions(game.id) + action_log = ActionLog( + index=0, + step=step, + type="temporary_rome_consul", + faction=temp_rome_consul_title.senator.faction, + data={"senator": temp_rome_consul_title.senator.id} + ) + action_log.save() + senator_action_log = SenatorActionLog( + senator=temp_rome_consul_title.senator, + action_log=action_log + ) + senator_action_log.save() + # Create potential actions for faction in factions: action = PotentialAction( diff --git a/frontend/components/notifications/Notification.tsx b/frontend/components/notifications/Notification.tsx index 97eab995..5cd63d8b 100644 --- a/frontend/components/notifications/Notification.tsx +++ b/frontend/components/notifications/Notification.tsx @@ -1,6 +1,7 @@ import ActionLog from "@/classes/ActionLog" import SelectFactionLeaderNotification from "./Notification_SelectFactionLeader" import FaceMortalityNotification from "./Notification_FaceMortality" +import TemporaryRomeConsulNotification from "./Notification_TemporaryRomeConsul" interface NotificationItemProps { notification: ActionLog @@ -8,7 +9,8 @@ interface NotificationItemProps { const notifications: { [key: string]: React.ComponentType<any> } = { select_faction_leader: SelectFactionLeaderNotification, - face_mortality: FaceMortalityNotification + face_mortality: FaceMortalityNotification, + temporary_rome_consul: TemporaryRomeConsulNotification } // Container for a notification, which determines the type of notification to render diff --git a/frontend/components/notifications/Notification_FaceMortality.tsx b/frontend/components/notifications/Notification_FaceMortality.tsx index 360c2298..946ea88a 100644 --- a/frontend/components/notifications/Notification_FaceMortality.tsx +++ b/frontend/components/notifications/Notification_FaceMortality.tsx @@ -1,9 +1,7 @@ import Image from 'next/image' import { Alert } from "@mui/material" - import ActionLog from "@/classes/ActionLog" import { useGameContext } from "@/contexts/GameContext" -import { useEffect, useState } from "react" import Faction from "@/classes/Faction" import DeadIcon from "@/images/icons/dead.svg" import styles from "./Notification.module.css" @@ -11,61 +9,46 @@ import Senator from '@/classes/Senator' import SenatorLink from "@/components/SenatorLink" import FactionLink from '@/components/FactionLink' -interface FaceMortalityNotificationProps { +interface NotificationProps { notification: ActionLog } -const FaceMortalityNotification = (props: FaceMortalityNotificationProps) => { +// Notification for when a senator dies during the mortality phase +const FaceMortalityNotification = ({ notification } : NotificationProps) => { const { allFactions, allSenators } = useGameContext() - - const [faction, setFaction] = useState<Faction | null>(null) - const [senator, setSenator] = useState<Senator | null>(null) - const [heir, setHeir] = useState<Senator | null>(null) - - // Update faction - useEffect(() => { - if (props.notification.faction) setFaction(allFactions.byId[props.notification.faction] ?? null) - }, [props.notification, allFactions, setFaction]) - - // Update senator and heir - useEffect(() => { - if (props.notification.data) { - setSenator(allSenators.byId[props.notification.data.senator] ?? null) - setHeir(allSenators.byId[props.notification.data.heir_senator] ?? null) - } - }, [props.notification, allSenators, setFaction, setHeir]) + // Get notification-specific data + const faction: Faction | null = notification.faction ? allFactions.byId[notification.faction] ?? null : null + const senator: Senator | null = notification.data.senator ? allSenators.byId[notification.data.senator] ?? null : null + const heir: Senator | null = notification.data.senator ? allSenators.byId[notification.data.heir_senator] ?? null : null + const majorOfficeName: string = notification.data.major_office + const getIcon = () => ( <div className={styles.icon}> <Image src={DeadIcon} alt="Dead" width={30} height={30} /> </div> ) - if (faction) { - const majorOffice: string = props.notification.data.major_office // This is just the name of the office - - if (senator) { - return ( - <Alert icon={getIcon()} style={{backgroundColor: faction.getColor("textBg")}}> - <b>Mortality</b> - <p> - <> - {majorOffice || heir ? <span>The</span> : null} - {majorOffice && <span> {majorOffice}</span>} - {majorOffice && heir ? <span> and</span> : null} - {heir && <span> <FactionLink faction={faction} /> Leader</span>} - {majorOffice || heir ? <span>, </span> : null} - <SenatorLink senator={senator} /> - {!heir && <span> of the <FactionLink faction={faction} /></span>} - <span> has passed away.</span> - {heir && <span> His heir <SenatorLink senator={heir} /> has replaced him as Faction Leader.</span>} - </> - </p> - </Alert> - ) - } - } - return null + if (!faction || !senator) return null + + return ( + <Alert icon={getIcon()} style={{backgroundColor: faction.getColor("textBg")}}> + <b>Mortality</b> + <p> + <> + {majorOfficeName || heir ? <span>The</span> : null} + {majorOfficeName && <span> {majorOfficeName}</span>} + {majorOfficeName && heir ? <span> and</span> : null} + {heir && <span> <FactionLink faction={faction} /> Leader</span>} + {majorOfficeName || heir ? <span>, </span> : null} + <SenatorLink senator={senator} /> + {!heir && <span> of the <FactionLink faction={faction} /></span>} + <span> has passed away.</span> + {heir && <span> His heir <SenatorLink senator={heir} /> has replaced him as Faction Leader.</span>} + </> + </p> + </Alert> + ) } export default FaceMortalityNotification diff --git a/frontend/components/notifications/Notification_SelectFactionLeader.tsx b/frontend/components/notifications/Notification_SelectFactionLeader.tsx index a8694739..8076a882 100644 --- a/frontend/components/notifications/Notification_SelectFactionLeader.tsx +++ b/frontend/components/notifications/Notification_SelectFactionLeader.tsx @@ -2,37 +2,24 @@ import { Alert } from "@mui/material" import ActionLog from "@/classes/ActionLog" import FactionIcon from "@/components/FactionIcon" import { useGameContext } from "@/contexts/GameContext" -import { useEffect, useState } from "react" import Faction from "@/classes/Faction" import Senator from "@/classes/Senator" import styles from "./Notification.module.css" import SenatorLink from "@/components/SenatorLink" import FactionLink from '@/components/FactionLink' -interface SelectFactionLeaderNotificationProps { +interface NotificationProps { notification: ActionLog } // Notification for when a new faction leader is selected -const SelectFactionLeaderNotification = (props: SelectFactionLeaderNotificationProps) => { +const SelectFactionLeaderNotification = ({ notification } : NotificationProps) => { const { allFactions, allSenators } = useGameContext() - const [faction, setFaction] = useState<Faction | null>(null) - const [oldFactionLeader, setOldFactionLeader] = useState<Senator | null>(null) - const [newFactionLeader, setNewFactionLeader] = useState<Senator | null>(null) - - // Update faction - useEffect(() => { - if (props.notification.faction) setFaction(allFactions.byId[props.notification.faction] ?? null) - }, [props.notification, allFactions, setFaction]) - - // Update old and new faction leaders - useEffect(() => { - if (props.notification.data) { - setOldFactionLeader(allSenators.byId[props.notification.data.previous_senator] ?? null) - setNewFactionLeader(allSenators.byId[props.notification.data.senator] ?? null) - } - }, [props.notification, allSenators, setOldFactionLeader, setNewFactionLeader]) + // Get notification-specific data + const faction: Faction | null = notification.faction ? allFactions.byId[notification.faction] ?? null : null + const oldFactionLeader: Senator | null = notification.data ? allSenators.byId[notification.data.previous_senator] ?? null : null + const newFactionLeader: Senator | null = notification.data ? allSenators.byId[notification.data.senator] ?? null : null const getIcon = () => { if (faction) { diff --git a/frontend/components/notifications/Notification_TemporaryRomeConsul.tsx b/frontend/components/notifications/Notification_TemporaryRomeConsul.tsx new file mode 100644 index 00000000..247748e0 --- /dev/null +++ b/frontend/components/notifications/Notification_TemporaryRomeConsul.tsx @@ -0,0 +1,46 @@ +import { Alert } from "@mui/material" +import ActionLog from "@/classes/ActionLog" +import SenatorLink from "@/components/SenatorLink" +import FactionLink from '@/components/FactionLink' +import Faction from "@/classes/Faction" +import Senator from '@/classes/Senator' +import { useGameContext } from "@/contexts/GameContext" +import styles from "./Notification.module.css" +import FactionIcon from "@/components/FactionIcon" + +interface NotificationProps { + notification: ActionLog +} + +const TemporaryRomeConsulNotification = ({ notification } : NotificationProps) => { + const { allFactions, allSenators } = useGameContext() + + // Get notification-specific data + const faction: Faction | null = notification.faction ? allFactions.byId[notification.faction] ?? null : null + const senator: Senator | null = notification.data.senator ? allSenators.byId[notification.data.senator] ?? null : null + + const getIcon = () => { + if (faction) { + return ( + <div className={styles.icon}> + <FactionIcon faction={faction} size={18} selectable /> + </div> + ) + } else { + return false + } + } + + if (!faction || !senator) return null + + return ( + <Alert icon={getIcon()} style={{backgroundColor: faction.getColor("textBg")}}> + <b>Temporary Rome Consul</b> + <p> + <SenatorLink senator={senator} /> of the <FactionLink faction={faction} /> now serves as our Temporary Rome Consul, making him the HRAO. + </p> + </Alert> + ) +} + +export default TemporaryRomeConsulNotification From a6a778ab649b9e5a77223885fe57980761e9ca3a Mon Sep 17 00:00:00 2001 From: Logan Davidson <iamlogandavidson@gmail.com> Date: Thu, 28 Sep 2023 19:44:25 +0100 Subject: [PATCH 12/21] Make the history tab look better Make the history tab look better. The tab now contains notification components, exactly like those in the notification section. However, these components have now been renamed to include the name "ActionLog" to reflect their dual/general purpose. --- frontend/components/FactionList.module.css | 2 +- frontend/components/ProgressSection.tsx | 4 +- frontend/components/SenatorList.module.css | 4 +- .../ActionLog.module.css} | 0 .../ActionLog.tsx} | 6 +- .../ActionLog_FaceMortality.tsx} | 2 +- .../ActionLog_SelectFactionLeader.tsx} | 2 +- .../ActionLog_TemporaryRomeConsul.tsx} | 2 +- .../detailSections/DetailSection.module.css | 2 +- .../DetailSection_Senator.module.css | 35 ++++++++- .../detailSections/DetailSection_Senator.tsx | 72 ++++++++++++------- frontend/contexts/GameContext.tsx | 6 +- 12 files changed, 96 insertions(+), 41 deletions(-) rename frontend/components/{notifications/Notification.module.css => actionLogs/ActionLog.module.css} (100%) rename frontend/components/{notifications/Notification.tsx => actionLogs/ActionLog.tsx} (74%) rename frontend/components/{notifications/Notification_FaceMortality.tsx => actionLogs/ActionLog_FaceMortality.tsx} (97%) rename frontend/components/{notifications/Notification_SelectFactionLeader.tsx => actionLogs/ActionLog_SelectFactionLeader.tsx} (97%) rename frontend/components/{notifications/Notification_TemporaryRomeConsul.tsx => actionLogs/ActionLog_TemporaryRomeConsul.tsx} (96%) diff --git a/frontend/components/FactionList.module.css b/frontend/components/FactionList.module.css index 97f7e2bd..cba0ce34 100644 --- a/frontend/components/FactionList.module.css +++ b/frontend/components/FactionList.module.css @@ -6,7 +6,7 @@ display: flex; flex-direction: column; border: 1px solid var(--divider-line-color); - border-radius: 4px; + border-radius: 3px; background-color: var(--background-color); margin: 8px; diff --git a/frontend/components/ProgressSection.tsx b/frontend/components/ProgressSection.tsx index 520eaf78..23b9efa3 100644 --- a/frontend/components/ProgressSection.tsx +++ b/frontend/components/ProgressSection.tsx @@ -13,7 +13,7 @@ import ActionDialog from "@/components/actionDialogs/ActionDialog" import ActionsType from "@/types/actions" import Faction from "@/classes/Faction" import ActionLog from "@/classes/ActionLog" -import NotificationContainer from "@/components/notifications/Notification" +import Notification from "@/components/actionLogs/ActionLog" const typedActions: ActionsType = Actions @@ -69,7 +69,7 @@ const ProgressSection = (props: ProgressSectionProps) => { <h3 style={{ lineHeight: '40px' }}>Notifications</h3> <div ref={notificationListRef} className={styles.notificationList}> {props.notifications && props.notifications.asArray.sort((a, b) => a.index - b.index).map((notification) => - <NotificationContainer key={notification.id} notification={notification} /> + <Notification key={notification.id} notification={notification} /> )} </div> </div> diff --git a/frontend/components/SenatorList.module.css b/frontend/components/SenatorList.module.css index 8cf0aaa7..86e1c2e7 100644 --- a/frontend/components/SenatorList.module.css +++ b/frontend/components/SenatorList.module.css @@ -6,7 +6,7 @@ display: flex; flex-direction: column; border: 1px solid var(--divider-line-color); - border-radius: 4px; + border-radius: 3px; background-color: var(--background-color); } @@ -37,7 +37,7 @@ flex-direction: row; gap: 2px; align-items: flex-start; - border-radius: 4px 4px 0 0; + border-radius: 3px 3px 0 0; overflow: hidden; } diff --git a/frontend/components/notifications/Notification.module.css b/frontend/components/actionLogs/ActionLog.module.css similarity index 100% rename from frontend/components/notifications/Notification.module.css rename to frontend/components/actionLogs/ActionLog.module.css diff --git a/frontend/components/notifications/Notification.tsx b/frontend/components/actionLogs/ActionLog.tsx similarity index 74% rename from frontend/components/notifications/Notification.tsx rename to frontend/components/actionLogs/ActionLog.tsx index 5cd63d8b..8aae22a6 100644 --- a/frontend/components/notifications/Notification.tsx +++ b/frontend/components/actionLogs/ActionLog.tsx @@ -1,7 +1,7 @@ import ActionLog from "@/classes/ActionLog" -import SelectFactionLeaderNotification from "./Notification_SelectFactionLeader" -import FaceMortalityNotification from "./Notification_FaceMortality" -import TemporaryRomeConsulNotification from "./Notification_TemporaryRomeConsul" +import SelectFactionLeaderNotification from "./ActionLog_SelectFactionLeader" +import FaceMortalityNotification from "./ActionLog_FaceMortality" +import TemporaryRomeConsulNotification from "./ActionLog_TemporaryRomeConsul" interface NotificationItemProps { notification: ActionLog diff --git a/frontend/components/notifications/Notification_FaceMortality.tsx b/frontend/components/actionLogs/ActionLog_FaceMortality.tsx similarity index 97% rename from frontend/components/notifications/Notification_FaceMortality.tsx rename to frontend/components/actionLogs/ActionLog_FaceMortality.tsx index 946ea88a..ab7ffc6a 100644 --- a/frontend/components/notifications/Notification_FaceMortality.tsx +++ b/frontend/components/actionLogs/ActionLog_FaceMortality.tsx @@ -4,7 +4,7 @@ import ActionLog from "@/classes/ActionLog" import { useGameContext } from "@/contexts/GameContext" import Faction from "@/classes/Faction" import DeadIcon from "@/images/icons/dead.svg" -import styles from "./Notification.module.css" +import styles from "./ActionLog.module.css" import Senator from '@/classes/Senator' import SenatorLink from "@/components/SenatorLink" import FactionLink from '@/components/FactionLink' diff --git a/frontend/components/notifications/Notification_SelectFactionLeader.tsx b/frontend/components/actionLogs/ActionLog_SelectFactionLeader.tsx similarity index 97% rename from frontend/components/notifications/Notification_SelectFactionLeader.tsx rename to frontend/components/actionLogs/ActionLog_SelectFactionLeader.tsx index 8076a882..c4927aab 100644 --- a/frontend/components/notifications/Notification_SelectFactionLeader.tsx +++ b/frontend/components/actionLogs/ActionLog_SelectFactionLeader.tsx @@ -4,7 +4,7 @@ import FactionIcon from "@/components/FactionIcon" import { useGameContext } from "@/contexts/GameContext" import Faction from "@/classes/Faction" import Senator from "@/classes/Senator" -import styles from "./Notification.module.css" +import styles from "./ActionLog.module.css" import SenatorLink from "@/components/SenatorLink" import FactionLink from '@/components/FactionLink' diff --git a/frontend/components/notifications/Notification_TemporaryRomeConsul.tsx b/frontend/components/actionLogs/ActionLog_TemporaryRomeConsul.tsx similarity index 96% rename from frontend/components/notifications/Notification_TemporaryRomeConsul.tsx rename to frontend/components/actionLogs/ActionLog_TemporaryRomeConsul.tsx index 247748e0..a394a7eb 100644 --- a/frontend/components/notifications/Notification_TemporaryRomeConsul.tsx +++ b/frontend/components/actionLogs/ActionLog_TemporaryRomeConsul.tsx @@ -5,7 +5,7 @@ import FactionLink from '@/components/FactionLink' import Faction from "@/classes/Faction" import Senator from '@/classes/Senator' import { useGameContext } from "@/contexts/GameContext" -import styles from "./Notification.module.css" +import styles from "./ActionLog.module.css" import FactionIcon from "@/components/FactionIcon" interface NotificationProps { diff --git a/frontend/components/detailSections/DetailSection.module.css b/frontend/components/detailSections/DetailSection.module.css index 3586064f..515eda36 100644 --- a/frontend/components/detailSections/DetailSection.module.css +++ b/frontend/components/detailSections/DetailSection.module.css @@ -36,7 +36,7 @@ box-sizing: border-box; height: 100%; flex-grow: 2; - padding: 8px; border-radius: 3px; background-color: var(--background-color-2); + overflow-y: auto; } diff --git a/frontend/components/detailSections/DetailSection_Senator.module.css b/frontend/components/detailSections/DetailSection_Senator.module.css index 1fdd7144..27ccd088 100644 --- a/frontend/components/detailSections/DetailSection_Senator.module.css +++ b/frontend/components/detailSections/DetailSection_Senator.module.css @@ -1,23 +1,44 @@ .senatorDetailSection { + height: 100%; + box-sizing: border-box; display: flex; flex-direction: column; gap: 8px; + overflow-y: auto; + padding: 8px 0; +} + +.primaryArea { + padding: 0 8px; } .portraitContainer { float: left; margin-right: 8px; - margin-bottom: 8px; + margin-bottom: 4px; } .textContainer>p:not(:last-child) { margin-bottom: 8px; } +.tabs { + padding: 0 8px; +} + +.tabContent { + flex-grow: 1; + overflow-y: auto; + background-color: var(--background-color); + border-radius: 3px; + margin: 0 8px; +} + .attributeArea { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 8px + gap: 8px; + overflow-y: auto; } .attributeArea>div { @@ -79,3 +100,13 @@ margin: 3px; line-height: 16px; } + +.logList { + height: 100%; + box-sizing: border-box; + padding: 8px; + display: flex; + flex-direction: column; + gap: 8px; + overflow-y: auto; +} diff --git a/frontend/components/detailSections/DetailSection_Senator.tsx b/frontend/components/detailSections/DetailSection_Senator.tsx index f11ad070..e2e727bd 100644 --- a/frontend/components/detailSections/DetailSection_Senator.tsx +++ b/frontend/components/detailSections/DetailSection_Senator.tsx @@ -1,5 +1,5 @@ import Image from 'next/image' -import { RefObject, useEffect, useState } from "react" +import { RefObject, useEffect, useRef, useState } from "react" import SenatorPortrait from "@/components/SenatorPortrait" import Senator from "@/classes/Senator" @@ -25,7 +25,7 @@ import { useAuthContext } from '@/contexts/AuthContext' import { deserializeToInstances } from '@/functions/serialize' import Collection from '@/classes/Collection' import SenatorActionLog from '@/classes/SenatorActionLog' -import { forEach } from 'lodash' +import Notification from "@/components/actionLogs/ActionLog" type FixedAttribute = { name: "military" | "oratory" | "loyalty" @@ -51,8 +51,19 @@ interface SenatorDetailsProps { // Detail section content for a senator const SenatorDetails = (props: SenatorDetailsProps) => { const { accessToken, refreshToken, setAccessToken, setRefreshToken, setUser } = useAuthContext() - const { allPlayers, allFactions, allSenators, setAllSenators, allTitles, selectedEntity, actionLogs, setActionLogs, senatorActionLogs, setSenatorActionLogs } = useGameContext() - + const { + allPlayers, + allFactions, + allSenators, setAllSenators, + allTitles, + selectedEntity, + actionLogs, setActionLogs, + senatorActionLogs, setSenatorActionLogs, + senatorDetailTab, setSenatorDetailTab + } = useGameContext() + + const scrollableAreaRef = useRef<HTMLDivElement | null>(null) + // Get senator-specific data const senator: Senator | null = selectedEntity?.id ? allSenators.byId[selectedEntity.id] ?? null : null const faction: Faction | null = senator?.faction ? allFactions.byId[senator.faction] ?? null : null @@ -62,9 +73,6 @@ const SenatorDetails = (props: SenatorDetailsProps) => { const matchingSenatorActionLogs = senator ? senatorActionLogs.asArray.filter(l => l.senator === senator.id) : null const matchingActionLogs = matchingSenatorActionLogs ? actionLogs.asArray.filter(a => matchingSenatorActionLogs.some(b => b.action_log === a.id)) : null - // UI selections - const [tab, setTab] = useState(0) - // Fetch action logs for this senator const fetchActionLogs = async () => { if (!senator) return @@ -96,7 +104,7 @@ const SenatorDetails = (props: SenatorDetailsProps) => { } } - // Fetch action logs is they haven't been fetched yet + // Fetch action logs and senator action logs if they haven't been fetched yet useEffect(() => { if (!senator || senator?.logsFetched) return @@ -109,6 +117,18 @@ const SenatorDetails = (props: SenatorDetailsProps) => { setAllSenators((senators: Collection<Senator>) => new Collection<Senator>(senators.asArray.map(s => s.id === senator.id ? senator : s))) }, [senator]) + // Initially scroll to bottom if history tab is selected + useEffect(() => { + if (scrollableAreaRef.current) { + scrollableAreaRef.current.scrollTop = scrollableAreaRef.current.scrollHeight + } + }, [senatorDetailTab]) + + // Change selected tab + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setSenatorDetailTab(newValue) + } + // Calculate senator portrait size. // Image size must be defined in JavaScript rather than in CSS const getPortraitSize = () => { @@ -123,10 +143,6 @@ const SenatorDetails = (props: SenatorDetailsProps) => { return 200 } } - - const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { - setTab(newValue) - } // Fixed attribute data const fixedAttributeItems: FixedAttribute[] = senator ? [ @@ -220,26 +236,30 @@ const SenatorDetails = (props: SenatorDetailsProps) => { </div> </div> <Box sx={{ borderBottom: 1, borderColor: 'divider' }}> - <Tabs value={tab} onChange={handleTabChange} className={styles.tabs}> + <Tabs value={senatorDetailTab} onChange={handleTabChange} className={styles.tabs}> <Tab label="Attributes" /> <Tab label="History"/> </Tabs> </Box> - {tab === 0 && - <div className={styles.attributeArea}> - <div className={styles.fixedAttributeContainer}> - {fixedAttributeItems.map(item => getFixedAttributeRow(item))} + <div className={styles.tabContent}> + {senatorDetailTab === 0 && + <div className={styles.attributeArea}> + <div className={styles.fixedAttributeContainer}> + {fixedAttributeItems.map(item => getFixedAttributeRow(item))} + </div> + <div className={styles.variableAttributeContainer}> + {variableAttributeItems.map(item => getVariableAttributeRow(item))} + </div> </div> - <div className={styles.variableAttributeContainer}> - {variableAttributeItems.map(item => getVariableAttributeRow(item))} + } + {senatorDetailTab === 1 && + <div ref={scrollableAreaRef} className={styles.logList}> + {matchingActionLogs && matchingActionLogs.sort((a, b) => a.index - b.index).map((notification) => + <Notification key={notification.id} notification={notification} /> + )} </div> - </div> - } - {tab === 1 && - <div> - {matchingActionLogs && matchingActionLogs.map((actionLog: ActionLog) => <i>{actionLog.type}</i>)} - </div> - } + } + </div> </div> ) } diff --git a/frontend/contexts/GameContext.tsx b/frontend/contexts/GameContext.tsx index cdff528a..9ec6a68a 100644 --- a/frontend/contexts/GameContext.tsx +++ b/frontend/contexts/GameContext.tsx @@ -29,6 +29,8 @@ interface GameContextType { setActionLogs: Dispatch<SetStateAction<Collection<ActionLog>>> senatorActionLogs: Collection<SenatorActionLog> setSenatorActionLogs: Dispatch<SetStateAction<Collection<SenatorActionLog>>> + senatorDetailTab: number + setSenatorDetailTab: Dispatch<SetStateAction<number>> } const GameContext = createContext<GameContextType | null>(null) @@ -57,6 +59,7 @@ export const GameProvider = ( props: GameProviderProps ): JSX.Element => { const [selectedEntity, setSelectedEntity] = useState<SelectedEntity | null>(null) const [actionLogs, setActionLogs] = useState<Collection<ActionLog>>(new Collection<ActionLog>()) const [senatorActionLogs, setSenatorActionLogs] = useState<Collection<SenatorActionLog>>(new Collection<SenatorActionLog>()) + const [senatorDetailTab, setSenatorDetailTab] = useState<number>(0) return ( <GameContext.Provider value={{ @@ -68,7 +71,8 @@ export const GameProvider = ( props: GameProviderProps ): JSX.Element => { allTitles, setAllTitles, selectedEntity, setSelectedEntity, actionLogs, setActionLogs, - senatorActionLogs, setSenatorActionLogs + senatorActionLogs, setSenatorActionLogs, + senatorDetailTab, setSenatorDetailTab }}> {props.children} </GameContext.Provider> From 6416aee8e4d36da40ace19f4b78539b8ac4d5500 Mon Sep 17 00:00:00 2001 From: Logan Davidson <iamlogandavidson@gmail.com> Date: Thu, 28 Sep 2023 19:58:55 +0100 Subject: [PATCH 13/21] Fix faction tab display issues --- frontend/components/SenatorList.tsx | 2 +- .../detailSections/DetailSection_Faction.module.css | 3 +++ frontend/components/detailSections/DetailSection_Faction.tsx | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/components/SenatorList.tsx b/frontend/components/SenatorList.tsx index 1b3c4720..d2785eb2 100644 --- a/frontend/components/SenatorList.tsx +++ b/frontend/components/SenatorList.tsx @@ -28,7 +28,7 @@ type SortAttribute = "military" | "oratory" | "loyalty" | "influence" | "talents const ICON_SIZE = 34 -const DEFAULT_MIN_HEIGHT = 260 +const DEFAULT_MIN_HEIGHT = 157 interface SenatorListProps { selectableSenators?: boolean diff --git a/frontend/components/detailSections/DetailSection_Faction.module.css b/frontend/components/detailSections/DetailSection_Faction.module.css index f87424d4..6939e991 100644 --- a/frontend/components/detailSections/DetailSection_Faction.module.css +++ b/frontend/components/detailSections/DetailSection_Faction.module.css @@ -1,5 +1,7 @@ .factionDetails { height: 100%; + box-sizing: border-box; + padding: 8px; display: flex; flex-direction: column; gap: 8px; @@ -19,4 +21,5 @@ .factionIcon { margin-right: 8px; + margin-top: 3px; } diff --git a/frontend/components/detailSections/DetailSection_Faction.tsx b/frontend/components/detailSections/DetailSection_Faction.tsx index 1791b816..e46ab771 100644 --- a/frontend/components/detailSections/DetailSection_Faction.tsx +++ b/frontend/components/detailSections/DetailSection_Faction.tsx @@ -27,7 +27,7 @@ const FactionDetails = () => { <div className={styles.mainArea}> <div className={styles.titleArea}> <span className={styles.factionIcon}> - <FactionIcon faction={faction} size={30} /> + <FactionIcon faction={faction} size={26} /> </span> <p><b>{faction.getName()} Faction</b> of {player.user?.username}</p> </div> @@ -35,7 +35,7 @@ const FactionDetails = () => { This faction has {senators.allIds.length} aligned senators </p> </div> - <SenatorList faction={faction} selectableSenators minHeight={360} /> + <SenatorList faction={faction} selectableSenators /> </div> ) From b462e0e3411a96a108e36f4b5e5ea6d2265ba6cd Mon Sep 17 00:00:00 2001 From: Logan Davidson <iamlogandavidson@gmail.com> Date: Thu, 28 Sep 2023 20:04:34 +0100 Subject: [PATCH 14/21] Fix action log duplication issue Fix an issue where duplicate action logs were appearing in the history tab. --- frontend/components/detailSections/DetailSection_Senator.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/components/detailSections/DetailSection_Senator.tsx b/frontend/components/detailSections/DetailSection_Senator.tsx index e2e727bd..9303d7da 100644 --- a/frontend/components/detailSections/DetailSection_Senator.tsx +++ b/frontend/components/detailSections/DetailSection_Senator.tsx @@ -81,7 +81,8 @@ const SenatorDetails = (props: SenatorDetailsProps) => { if (response.status === 200) { const deserializedInstances = deserializeToInstances<ActionLog>(ActionLog, response.data) setActionLogs((instances: Collection<ActionLog>) => - new Collection<ActionLog>(instances.asArray.concat(deserializedInstances)) + // Loop over each instance in deserializedInstances and add it to the collection if it's not already there + new Collection<ActionLog>(instances.asArray.concat(deserializedInstances.filter(i => !instances.asArray.some(j => j.id === i.id)))) ) } else { setActionLogs(new Collection<ActionLog>()) From 09320fc46831b7dfe2ab0d6ede699dbd59efcf1c Mon Sep 17 00:00:00 2001 From: Logan Davidson <iamlogandavidson@gmail.com> Date: Thu, 28 Sep 2023 20:42:25 +0100 Subject: [PATCH 15/21] Past tense action log messages Added past tense versions of all action log messages, for the senator history tab. --- frontend/components/actionLogs/ActionLog.tsx | 3 +- .../actionLogs/ActionLog_FaceMortality.tsx | 36 +++++++++++-------- .../ActionLog_SelectFactionLeader.tsx | 35 +++++++++++++----- .../ActionLog_TemporaryRomeConsul.tsx | 26 +++++++++++--- .../detailSections/DetailSection_Senator.tsx | 4 +-- 5 files changed, 74 insertions(+), 30 deletions(-) diff --git a/frontend/components/actionLogs/ActionLog.tsx b/frontend/components/actionLogs/ActionLog.tsx index 8aae22a6..4a22964f 100644 --- a/frontend/components/actionLogs/ActionLog.tsx +++ b/frontend/components/actionLogs/ActionLog.tsx @@ -5,6 +5,7 @@ import TemporaryRomeConsulNotification from "./ActionLog_TemporaryRomeConsul" interface NotificationItemProps { notification: ActionLog + senatorDetails?: boolean } const notifications: { [key: string]: React.ComponentType<any> } = { @@ -17,7 +18,7 @@ const notifications: { [key: string]: React.ComponentType<any> } = { const NotificationContainer = (props: NotificationItemProps) => { const ContentComponent = notifications[props.notification.type] return ( - <ContentComponent notification={props.notification}/> + <ContentComponent notification={props.notification} senatorDetails={props.senatorDetails} /> ) } diff --git a/frontend/components/actionLogs/ActionLog_FaceMortality.tsx b/frontend/components/actionLogs/ActionLog_FaceMortality.tsx index ab7ffc6a..0bb26ae6 100644 --- a/frontend/components/actionLogs/ActionLog_FaceMortality.tsx +++ b/frontend/components/actionLogs/ActionLog_FaceMortality.tsx @@ -11,10 +11,11 @@ import FactionLink from '@/components/FactionLink' interface NotificationProps { notification: ActionLog + senatorDetails?: boolean } // Notification for when a senator dies during the mortality phase -const FaceMortalityNotification = ({ notification } : NotificationProps) => { +const FaceMortalityNotification = ({ notification, senatorDetails } : NotificationProps) => { const { allFactions, allSenators } = useGameContext() // Get notification-specific data @@ -29,24 +30,31 @@ const FaceMortalityNotification = ({ notification } : NotificationProps) => { </div> ) + // Get the text for the notification (tense sensitive) + const getText = () => { + if (!faction || !senator) return null + + return ( + <p> + {majorOfficeName || heir ? <span>The</span> : null} + {majorOfficeName && <span> {majorOfficeName}</span>} + {majorOfficeName && heir ? <span> and</span> : null} + {heir && <span> <FactionLink faction={faction} /> Leader</span>} + {majorOfficeName || heir ? <span>, </span> : null} + <SenatorLink senator={senator} /> + {!heir && <span> of the <FactionLink faction={faction} /></span>} + <span> {!senatorDetails && "has"} passed away.</span> + {heir && <span> His heir <SenatorLink senator={heir} /> {!senatorDetails && "has"} replaced him as Faction Leader.</span>} + </p> + ) + } + if (!faction || !senator) return null return ( <Alert icon={getIcon()} style={{backgroundColor: faction.getColor("textBg")}}> <b>Mortality</b> - <p> - <> - {majorOfficeName || heir ? <span>The</span> : null} - {majorOfficeName && <span> {majorOfficeName}</span>} - {majorOfficeName && heir ? <span> and</span> : null} - {heir && <span> <FactionLink faction={faction} /> Leader</span>} - {majorOfficeName || heir ? <span>, </span> : null} - <SenatorLink senator={senator} /> - {!heir && <span> of the <FactionLink faction={faction} /></span>} - <span> has passed away.</span> - {heir && <span> His heir <SenatorLink senator={heir} /> has replaced him as Faction Leader.</span>} - </> - </p> + {getText()} </Alert> ) } diff --git a/frontend/components/actionLogs/ActionLog_SelectFactionLeader.tsx b/frontend/components/actionLogs/ActionLog_SelectFactionLeader.tsx index c4927aab..aaf4481f 100644 --- a/frontend/components/actionLogs/ActionLog_SelectFactionLeader.tsx +++ b/frontend/components/actionLogs/ActionLog_SelectFactionLeader.tsx @@ -10,10 +10,11 @@ import FactionLink from '@/components/FactionLink' interface NotificationProps { notification: ActionLog + senatorDetails?: boolean } // Notification for when a new faction leader is selected -const SelectFactionLeaderNotification = ({ notification } : NotificationProps) => { +const SelectFactionLeaderNotification = ({ notification, senatorDetails } : NotificationProps) => { const { allFactions, allSenators } = useGameContext() // Get notification-specific data @@ -33,19 +34,35 @@ const SelectFactionLeaderNotification = ({ notification } : NotificationProps) = } } - if (faction && newFactionLeader) { - return ( - <Alert icon={getIcon()} style={{backgroundColor: faction.getColor("textBg")}}> - <b>New Faction Leader</b> + // Get the text for the notification (tense sensitive) + const getText = () => { + if (!newFactionLeader || !faction) return null + + if (senatorDetails) { + return ( <p> - <SenatorLink senator={newFactionLeader} /> now holds the position of <FactionLink faction={faction} /> Leader + <SenatorLink senator={newFactionLeader} /> became <FactionLink faction={faction} /> Leader {oldFactionLeader ? ', taking over from ' + <SenatorLink senator={oldFactionLeader} /> + '.' : '.'} </p> - </Alert> - ) + ) } else { - return null + return ( + <p> + <SenatorLink senator={newFactionLeader} /> now holds the position of <FactionLink faction={faction} /> Leader + {oldFactionLeader ? ', taking over from ' + <SenatorLink senator={oldFactionLeader} /> + '.' : '.'} + </p> + ) + } } + + if (!newFactionLeader || !faction) return null + + return ( + <Alert icon={getIcon()} style={{backgroundColor: faction.getColor("textBg")}}> + <b>New Faction Leader</b> + {getText()} + </Alert> + ) } export default SelectFactionLeaderNotification diff --git a/frontend/components/actionLogs/ActionLog_TemporaryRomeConsul.tsx b/frontend/components/actionLogs/ActionLog_TemporaryRomeConsul.tsx index a394a7eb..2f1b3b5b 100644 --- a/frontend/components/actionLogs/ActionLog_TemporaryRomeConsul.tsx +++ b/frontend/components/actionLogs/ActionLog_TemporaryRomeConsul.tsx @@ -10,9 +10,10 @@ import FactionIcon from "@/components/FactionIcon" interface NotificationProps { notification: ActionLog + senatorDetails?: boolean } -const TemporaryRomeConsulNotification = ({ notification } : NotificationProps) => { +const TemporaryRomeConsulNotification = ({ notification, senatorDetails } : NotificationProps) => { const { allFactions, allSenators } = useGameContext() // Get notification-specific data @@ -31,14 +32,31 @@ const TemporaryRomeConsulNotification = ({ notification } : NotificationProps) = } } + // Get the text for the notification (tense sensitive) + const getText = () => { + if (!faction || !senator) return null + + if (senatorDetails) { + return ( + <p> + <SenatorLink senator={senator} /> became Temporary Rome Consul. + </p> + ) + } else { + return ( + <p> + <SenatorLink senator={senator} /> of the <FactionLink faction={faction} /> now holds the office of Temporary Rome Consul, making him the HRAO. + </p> + ) + } + } + if (!faction || !senator) return null return ( <Alert icon={getIcon()} style={{backgroundColor: faction.getColor("textBg")}}> <b>Temporary Rome Consul</b> - <p> - <SenatorLink senator={senator} /> of the <FactionLink faction={faction} /> now serves as our Temporary Rome Consul, making him the HRAO. - </p> + {getText()} </Alert> ) } diff --git a/frontend/components/detailSections/DetailSection_Senator.tsx b/frontend/components/detailSections/DetailSection_Senator.tsx index 9303d7da..8baf296a 100644 --- a/frontend/components/detailSections/DetailSection_Senator.tsx +++ b/frontend/components/detailSections/DetailSection_Senator.tsx @@ -25,7 +25,7 @@ import { useAuthContext } from '@/contexts/AuthContext' import { deserializeToInstances } from '@/functions/serialize' import Collection from '@/classes/Collection' import SenatorActionLog from '@/classes/SenatorActionLog' -import Notification from "@/components/actionLogs/ActionLog" +import ActionLogContainer from "@/components/actionLogs/ActionLog" type FixedAttribute = { name: "military" | "oratory" | "loyalty" @@ -256,7 +256,7 @@ const SenatorDetails = (props: SenatorDetailsProps) => { {senatorDetailTab === 1 && <div ref={scrollableAreaRef} className={styles.logList}> {matchingActionLogs && matchingActionLogs.sort((a, b) => a.index - b.index).map((notification) => - <Notification key={notification.id} notification={notification} /> + <ActionLogContainer key={notification.id} notification={notification} senatorDetails /> )} </div> } From 2a0aa098dd065eeafb8f17b8c820e0279491ac7c Mon Sep 17 00:00:00 2001 From: Logan Davidson <iamlogandavidson@gmail.com> Date: Thu, 28 Sep 2023 21:17:16 +0100 Subject: [PATCH 16/21] Add no history placeholder Add a placeholder for senators with no related action logs (i.e. no "history"). --- .../DetailSection_Senator.module.css | 8 +++++ .../detailSections/DetailSection_Senator.tsx | 29 ++++++++++++++----- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/frontend/components/detailSections/DetailSection_Senator.module.css b/frontend/components/detailSections/DetailSection_Senator.module.css index 27ccd088..d1e5ab12 100644 --- a/frontend/components/detailSections/DetailSection_Senator.module.css +++ b/frontend/components/detailSections/DetailSection_Senator.module.css @@ -110,3 +110,11 @@ gap: 8px; overflow-y: auto; } + +.noHistory { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + font-size: 14px; +} diff --git a/frontend/components/detailSections/DetailSection_Senator.tsx b/frontend/components/detailSections/DetailSection_Senator.tsx index 8baf296a..f04b5a9c 100644 --- a/frontend/components/detailSections/DetailSection_Senator.tsx +++ b/frontend/components/detailSections/DetailSection_Senator.tsx @@ -64,6 +64,8 @@ const SenatorDetails = (props: SenatorDetailsProps) => { const scrollableAreaRef = useRef<HTMLDivElement | null>(null) + const [fetchingLogs, setFetchingLogs] = useState(false) + // Get senator-specific data const senator: Senator | null = selectedEntity?.id ? allSenators.byId[selectedEntity.id] ?? null : null const faction: Faction | null = senator?.faction ? allFactions.byId[senator.faction] ?? null : null @@ -105,17 +107,26 @@ const SenatorDetails = (props: SenatorDetailsProps) => { } } - // Fetch action logs and senator action logs if they haven't been fetched yet - useEffect(() => { - if (!senator || senator?.logsFetched) return + // Fetch logs + const fetchLogs = async () => { + if (!senator) return - // Fetch action logs and senator action logs - fetchActionLogs() - fetchSenatorActionLogs() + await Promise.all([fetchActionLogs(), fetchSenatorActionLogs()]) + setFetchingLogs(false) - // Set logsFetched to true so that we don't fetch again + // Set logsFetched to true for this senator so that we don't fetch logs for him again senator.logsFetched = true setAllSenators((senators: Collection<Senator>) => new Collection<Senator>(senators.asArray.map(s => s.id === senator.id ? senator : s))) + } + + // Fetch logs once component mounts, but only if they haven't been fetched yet + useEffect(() => { + if (!senator || senator?.logsFetched) return + + setFetchingLogs(true) + + // Fetch action logs and senator action logs + fetchLogs() }, [senator]) // Initially scroll to bottom if history tab is selected @@ -258,6 +269,10 @@ const SenatorDetails = (props: SenatorDetailsProps) => { {matchingActionLogs && matchingActionLogs.sort((a, b) => a.index - b.index).map((notification) => <ActionLogContainer key={notification.id} notification={notification} senatorDetails /> )} + + {matchingActionLogs && matchingActionLogs.length === 0 && senator.logsFetched && + <div className={styles.noHistory}>{senator.displayName} has not yet made his name</div> + } </div> } </div> From 4165acfe775398906099cc38641650191e650d79 Mon Sep 17 00:00:00 2001 From: Logan Davidson <iamlogandavidson@gmail.com> Date: Fri, 29 Sep 2023 08:28:36 +0100 Subject: [PATCH 17/21] Fix display issue with the lobby button --- frontend/components/MetaSection.module.css | 6 ++++++ frontend/components/MetaSection.tsx | 8 +++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/components/MetaSection.module.css b/frontend/components/MetaSection.module.css index 7de2910a..e59ecf06 100644 --- a/frontend/components/MetaSection.module.css +++ b/frontend/components/MetaSection.module.css @@ -21,6 +21,12 @@ justify-content: space-between; } +.lobbyButton { + display: flex; + flex-direction: column; + justify-content: center; +} + .otherInfo { flex-grow: 1; padding: 16px; diff --git a/frontend/components/MetaSection.tsx b/frontend/components/MetaSection.tsx index 8f1d3212..3e97c6b0 100644 --- a/frontend/components/MetaSection.tsx +++ b/frontend/components/MetaSection.tsx @@ -46,9 +46,11 @@ const MetaSection = (props: MetaSectionProps) => { Turn {props.latestTurn.index}, {props.latestPhase.name} Phase </span> </div> - <Button variant="outlined" LinkComponent={Link} href={`/games/${game.id}`}> - <FontAwesomeIcon icon={faRightFromBracket} fontSize={16} style={{marginRight: 8}} />Lobby - </Button> + <div className={styles.lobbyButton}> + <Button variant="outlined" LinkComponent={Link} href={`/games/${game.id}`}> + <FontAwesomeIcon icon={faRightFromBracket} fontSize={16} style={{marginRight: 8}} />Lobby + </Button> + </div> </div> <div className={styles.otherInfo}> {faction && <div><span>Playing as the <FactionLink faction={faction} includeIcon /></span></div>} From 433d25e577c56457868e690d4517ad0d6676af42 Mon Sep 17 00:00:00 2001 From: Logan Davidson <iamlogandavidson@gmail.com> Date: Fri, 29 Sep 2023 08:44:22 +0100 Subject: [PATCH 18/21] Use Rome Consul icon for action log Set the icon for the Temporary Rome Consul action log to the Rome Consul icon. --- .../actionLogs/ActionLog.module.css | 15 ++------------- .../ActionLog_SelectFactionLeader.tsx | 15 ++++++--------- .../ActionLog_TemporaryRomeConsul.tsx | 19 +++++++------------ 3 files changed, 15 insertions(+), 34 deletions(-) diff --git a/frontend/components/actionLogs/ActionLog.module.css b/frontend/components/actionLogs/ActionLog.module.css index 948fa6e4..db2bcbc6 100644 --- a/frontend/components/actionLogs/ActionLog.module.css +++ b/frontend/components/actionLogs/ActionLog.module.css @@ -1,17 +1,6 @@ .icon { - position: relative; height: 18px; width: 24px; - z-index: 1; -} - -.icon>* { - position: absolute; - top: 2px; - left: 4px; -} - -.icon>img { - left: 50%; - transform: translate(-50%, 0); + display: flex; + justify-content: center; } diff --git a/frontend/components/actionLogs/ActionLog_SelectFactionLeader.tsx b/frontend/components/actionLogs/ActionLog_SelectFactionLeader.tsx index aaf4481f..98a33e17 100644 --- a/frontend/components/actionLogs/ActionLog_SelectFactionLeader.tsx +++ b/frontend/components/actionLogs/ActionLog_SelectFactionLeader.tsx @@ -23,15 +23,12 @@ const SelectFactionLeaderNotification = ({ notification, senatorDetails } : Noti const newFactionLeader: Senator | null = notification.data ? allSenators.byId[notification.data.senator] ?? null : null const getIcon = () => { - if (faction) { - return ( - <div className={styles.icon}> - <FactionIcon faction={faction} size={18} selectable /> - </div> - ) - } else { - return false - } + if (!faction) return null + return ( + <div className={styles.icon} style={{ marginTop: 4 }}> + <FactionIcon faction={faction} size={22} selectable /> + </div> + ) } // Get the text for the notification (tense sensitive) diff --git a/frontend/components/actionLogs/ActionLog_TemporaryRomeConsul.tsx b/frontend/components/actionLogs/ActionLog_TemporaryRomeConsul.tsx index 2f1b3b5b..a7e055fe 100644 --- a/frontend/components/actionLogs/ActionLog_TemporaryRomeConsul.tsx +++ b/frontend/components/actionLogs/ActionLog_TemporaryRomeConsul.tsx @@ -1,12 +1,13 @@ +import Image from 'next/image' import { Alert } from "@mui/material" import ActionLog from "@/classes/ActionLog" import SenatorLink from "@/components/SenatorLink" import FactionLink from '@/components/FactionLink' +import RomeConsulIcon from "@/images/icons/romeConsul.svg" import Faction from "@/classes/Faction" import Senator from '@/classes/Senator' import { useGameContext } from "@/contexts/GameContext" import styles from "./ActionLog.module.css" -import FactionIcon from "@/components/FactionIcon" interface NotificationProps { notification: ActionLog @@ -20,17 +21,11 @@ const TemporaryRomeConsulNotification = ({ notification, senatorDetails } : Noti const faction: Faction | null = notification.faction ? allFactions.byId[notification.faction] ?? null : null const senator: Senator | null = notification.data.senator ? allSenators.byId[notification.data.senator] ?? null : null - const getIcon = () => { - if (faction) { - return ( - <div className={styles.icon}> - <FactionIcon faction={faction} size={18} selectable /> - </div> - ) - } else { - return false - } - } + const getIcon = () => ( + <div className={styles.icon}> + <Image src={RomeConsulIcon} alt="Dead" width={30} height={30} /> + </div> + ) // Get the text for the notification (tense sensitive) const getText = () => { From d73d7b3daba7e52de9d9db0231d3ed87964cb38f Mon Sep 17 00:00:00 2001 From: Logan Davidson <iamlogandavidson@gmail.com> Date: Fri, 29 Sep 2023 08:53:03 +0100 Subject: [PATCH 19/21] Fix frontend build warnings --- frontend/components/GamePage.tsx | 4 +++- .../detailSections/DetailSection_Senator.tsx | 21 +++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/frontend/components/GamePage.tsx b/frontend/components/GamePage.tsx index 097ecf65..61bccfcf 100644 --- a/frontend/components/GamePage.tsx +++ b/frontend/components/GamePage.tsx @@ -452,7 +452,9 @@ const GamePage = (props: GamePageProps) => { } } } - }, [lastMessage, game?.id, setLatestTurn, setLatestPhase, setLatestStep, setPotentialActions, setAllTitles, setAllSenators, setNotifications]) + }, [ + lastMessage, + game?.id, setLatestTurn, setLatestPhase, setLatestStep, setPotentialActions, setAllTitles, setAllSenators, setNotifications, setActionLogs, setSenatorActionLogs]) const handleMainTabChange = (event: React.SyntheticEvent, newValue: number) => { setMainTab(newValue) diff --git a/frontend/components/detailSections/DetailSection_Senator.tsx b/frontend/components/detailSections/DetailSection_Senator.tsx index f04b5a9c..a0718085 100644 --- a/frontend/components/detailSections/DetailSection_Senator.tsx +++ b/frontend/components/detailSections/DetailSection_Senator.tsx @@ -1,5 +1,5 @@ import Image from 'next/image' -import { RefObject, useEffect, useRef, useState } from "react" +import { RefObject, useCallback, useEffect, useRef, useState } from "react" import SenatorPortrait from "@/components/SenatorPortrait" import Senator from "@/classes/Senator" @@ -76,7 +76,7 @@ const SenatorDetails = (props: SenatorDetailsProps) => { const matchingActionLogs = matchingSenatorActionLogs ? actionLogs.asArray.filter(a => matchingSenatorActionLogs.some(b => b.action_log === a.id)) : null // Fetch action logs for this senator - const fetchActionLogs = async () => { + const fetchActionLogs = useCallback(async () => { if (!senator) return const response = await request('GET', `action-logs/?senator=${senator.id}`, accessToken, refreshToken, setAccessToken, setRefreshToken, setUser) @@ -89,10 +89,10 @@ const SenatorDetails = (props: SenatorDetailsProps) => { } else { setActionLogs(new Collection<ActionLog>()) } - } + }, [senator, accessToken, refreshToken, setAccessToken, setRefreshToken, setUser, setActionLogs]) // Fetch senator action logs for this senator - const fetchSenatorActionLogs = async () => { + const fetchSenatorActionLogs = useCallback(async () => { if (!senator) return const response = await request('GET', `senator-action-logs/?senator=${senator.id}`, accessToken, refreshToken, setAccessToken, setRefreshToken, setUser) @@ -105,19 +105,18 @@ const SenatorDetails = (props: SenatorDetailsProps) => { } else { setSenatorActionLogs(new Collection<SenatorActionLog>()) } - } + }, [senator, accessToken, refreshToken, setAccessToken, setRefreshToken, setUser, setSenatorActionLogs]) // Fetch logs - const fetchLogs = async () => { + const fetchLogs = useCallback(async () => { if (!senator) return - + await Promise.all([fetchActionLogs(), fetchSenatorActionLogs()]) setFetchingLogs(false) - - // Set logsFetched to true for this senator so that we don't fetch logs for him again + senator.logsFetched = true setAllSenators((senators: Collection<Senator>) => new Collection<Senator>(senators.asArray.map(s => s.id === senator.id ? senator : s))) - } + }, [senator, fetchActionLogs, fetchSenatorActionLogs, setAllSenators]) // Fetch logs once component mounts, but only if they haven't been fetched yet useEffect(() => { @@ -127,7 +126,7 @@ const SenatorDetails = (props: SenatorDetailsProps) => { // Fetch action logs and senator action logs fetchLogs() - }, [senator]) + }, [senator, fetchLogs]) // Initially scroll to bottom if history tab is selected useEffect(() => { From d1cde0edccc252491a2c9a2a5eb2d238639c8e69 Mon Sep 17 00:00:00 2001 From: Logan Davidson <iamlogandavidson@gmail.com> Date: Fri, 29 Sep 2023 09:01:21 +0100 Subject: [PATCH 20/21] Fix image alt values for accessibility --- frontend/components/SenatorList.tsx | 2 +- frontend/components/SenatorPortrait.tsx | 4 ++-- frontend/components/TitleIcon.tsx | 2 +- .../components/actionDialogs/ActionDialog_FaceMortality.tsx | 2 +- frontend/components/actionLogs/ActionLog_FaceMortality.tsx | 2 +- .../components/actionLogs/ActionLog_TemporaryRomeConsul.tsx | 2 +- frontend/components/detailSections/DetailSection_Senator.tsx | 4 ++-- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/components/SenatorList.tsx b/frontend/components/SenatorList.tsx index d2785eb2..f7dcba40 100644 --- a/frontend/components/SenatorList.tsx +++ b/frontend/components/SenatorList.tsx @@ -170,7 +170,7 @@ const SenatorList = (props: SenatorListProps) => { return ( <Tooltip key={header.name} title={titleCaseName} enterDelay={500} placement="top" arrow> <button onClick={() => handleSortClick(header.name)} className={styles.header}> - <Image src={header.icon} height={ICON_SIZE} width={ICON_SIZE} alt={`${titleCaseName} Icon`} /> + <Image src={header.icon} height={ICON_SIZE} width={ICON_SIZE} alt={`${titleCaseName} icon`} /> {sort === header.name && <FontAwesomeIcon icon={faChevronUp} fontSize={18} />} {sort === `-${header.name}` && <FontAwesomeIcon icon={faChevronDown} fontSize={18} />} </button> diff --git a/frontend/components/SenatorPortrait.tsx b/frontend/components/SenatorPortrait.tsx index 6da73252..b43b1d0a 100644 --- a/frontend/components/SenatorPortrait.tsx +++ b/frontend/components/SenatorPortrait.tsx @@ -205,7 +205,7 @@ const SenatorPortrait = ({ senator, size, selectable }: SenatorPortraitProps) => <Image src={FactionLeaderPattern} className={styles.factionLeaderPattern} - alt="Faction Leader" + alt="Faction Leader pattern" /> } <Image @@ -226,7 +226,7 @@ const SenatorPortrait = ({ senator, size, selectable }: SenatorPortraitProps) => } {majorOffice && <TitleIcon title={majorOffice} size={getIconSize()} />} {senator.alive === false && - <Image src={DeadIcon} alt="Dead" height={getIconSize()} className={styles.deadIcon} /> + <Image src={DeadIcon} alt="Skull and crossbones icon" height={getIconSize()} className={styles.deadIcon} /> } </figure> </PortraitElement> diff --git a/frontend/components/TitleIcon.tsx b/frontend/components/TitleIcon.tsx index be6c734a..fed2b6de 100644 --- a/frontend/components/TitleIcon.tsx +++ b/frontend/components/TitleIcon.tsx @@ -10,7 +10,7 @@ interface TitleIconProps { const TitleIcon = (props: TitleIconProps) => { if (props.title.name.includes("Rome Consul")) { - return <Image className={styles.titleIcon} src={RomeConsulIcon} height={props.size} width={props.size} alt="Rome Consul" /> + return <Image className={styles.titleIcon} src={RomeConsulIcon} height={props.size} width={props.size} alt="Rome Consul icon" /> } else { return null; } diff --git a/frontend/components/actionDialogs/ActionDialog_FaceMortality.tsx b/frontend/components/actionDialogs/ActionDialog_FaceMortality.tsx index e8f1e93b..212a84f2 100644 --- a/frontend/components/actionDialogs/ActionDialog_FaceMortality.tsx +++ b/frontend/components/actionDialogs/ActionDialog_FaceMortality.tsx @@ -53,7 +53,7 @@ const FaceMortalityDialog = (props: FaceMortalityDialogProps ) => { </blockquote> <div> - <div style={{ float: 'left', marginRight: 8 }}><Image src={DeadIcon} alt="a" height={70} /></div> + <div style={{ float: 'left', marginRight: 8 }}><Image src={DeadIcon} alt="Skull and crossbones icon" height={70} /></div> <p>One or more senators may randomly die. When a family senator dies, their heir may return to play later as an unaligned senator. When a statesman dies, they never return.</p> </div> </DialogContent> diff --git a/frontend/components/actionLogs/ActionLog_FaceMortality.tsx b/frontend/components/actionLogs/ActionLog_FaceMortality.tsx index 0bb26ae6..dede2473 100644 --- a/frontend/components/actionLogs/ActionLog_FaceMortality.tsx +++ b/frontend/components/actionLogs/ActionLog_FaceMortality.tsx @@ -26,7 +26,7 @@ const FaceMortalityNotification = ({ notification, senatorDetails } : Notificati const getIcon = () => ( <div className={styles.icon}> - <Image src={DeadIcon} alt="Dead" width={30} height={30} /> + <Image src={DeadIcon} alt="Skull and crossbones icon" width={30} height={30} /> </div> ) diff --git a/frontend/components/actionLogs/ActionLog_TemporaryRomeConsul.tsx b/frontend/components/actionLogs/ActionLog_TemporaryRomeConsul.tsx index a7e055fe..662e144d 100644 --- a/frontend/components/actionLogs/ActionLog_TemporaryRomeConsul.tsx +++ b/frontend/components/actionLogs/ActionLog_TemporaryRomeConsul.tsx @@ -23,7 +23,7 @@ const TemporaryRomeConsulNotification = ({ notification, senatorDetails } : Noti const getIcon = () => ( <div className={styles.icon}> - <Image src={RomeConsulIcon} alt="Dead" width={30} height={30} /> + <Image src={RomeConsulIcon} alt="Rome Consul icon" width={30} height={30} /> </div> ) diff --git a/frontend/components/detailSections/DetailSection_Senator.tsx b/frontend/components/detailSections/DetailSection_Senator.tsx index a0718085..b1b5c891 100644 --- a/frontend/components/detailSections/DetailSection_Senator.tsx +++ b/frontend/components/detailSections/DetailSection_Senator.tsx @@ -189,7 +189,7 @@ const SenatorDetails = (props: SenatorDetailsProps) => { <div> {titleCaseName} </div> - <Image src={item.image} height={34} width={34} alt={`${titleCaseName} Icon`} style={{ userSelect: 'none' }} /> + <Image src={item.image} height={34} width={34} alt={`${titleCaseName} icon`} style={{ userSelect: 'none' }} /> <div><i>{item.description}</i></div> <div className={styles.skill} @@ -217,7 +217,7 @@ const SenatorDetails = (props: SenatorDetailsProps) => { return ( <div> <div>{titleCaseName}</div> - <Image src={item.image} height={34} width={34} alt={`${titleCaseName} Icon`} style={{ userSelect: 'none' }} /> + <Image src={item.image} height={34} width={34} alt={`${titleCaseName} icon`} style={{ userSelect: 'none' }} /> <div className={styles.attributeValue}>{item.value}</div> </div> ) From 7ae6bcf32be07506eb028ad7eefdee61eefb4f73 Mon Sep 17 00:00:00 2001 From: Logan Davidson <iamlogandavidson@gmail.com> Date: Fri, 29 Sep 2023 12:56:19 +0100 Subject: [PATCH 21/21] Prevent creation of duplicate Temp RC action logs --- ..._rename_notification_actionlog_and_more.py | 59 +++++++++++++++++++ backend/rorapp/models/senator_action_log.py | 2 +- backend/rorapp/views/start_game.py | 7 ++- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/backend/rorapp/migrations/0034_senatoractionlog_rename_notification_actionlog_and_more.py b/backend/rorapp/migrations/0034_senatoractionlog_rename_notification_actionlog_and_more.py index 21d44656..2c5d448d 100644 --- a/backend/rorapp/migrations/0034_senatoractionlog_rename_notification_actionlog_and_more.py +++ b/backend/rorapp/migrations/0034_senatoractionlog_rename_notification_actionlog_and_more.py @@ -4,6 +4,59 @@ import django.db.models.deletion +def create_temp_rc_log(apps, schema_editor): + + # Get relevant models + Title = apps.get_model('rorapp', 'Title') + Senator = apps.get_model('rorapp', 'Senator') + ActionLog = apps.get_model('rorapp', 'ActionLog') + SenatorActionLog = apps.get_model('rorapp', 'SenatorActionLog') + Step = apps.get_model('rorapp', 'Step') + + # Get all temporary rome consul titles + temp_rome_consul_titles = Title.objects.filter(name="Temporary Rome Consul") + + # Get all temporary rome consuls + temp_rome_consuls = Senator.objects.filter(titles__in=temp_rome_consul_titles) + + # Loop over each temporary rome consul + for temp_rome_consul in temp_rome_consuls: + game = temp_rome_consul.game + + step = Step.objects.get(index=0, phase__turn__game=game) + temp_rome_consul_title = temp_rome_consul_titles.get(senator=temp_rome_consul) + + # Create action logs for temporary rome consul + action_log = ActionLog( + index=0, + step=step, + type="temporary_rome_consul", + faction=temp_rome_consul_title.senator.faction, + data={"senator": temp_rome_consul_title.senator.id} + ) + action_log.save() + senator_action_log = SenatorActionLog( + senator=temp_rome_consul_title.senator, + action_log=action_log + ) + senator_action_log.save() + + +def remove_temp_rc_log(apps, schema_editor): + + # Get relevant models + ActionLog = apps.get_model('rorapp', 'ActionLog') + SenatorActionLog = apps.get_model('rorapp', 'SenatorActionLog') + + temp_rc_action_logs = ActionLog.objects.filter(type="temporary_rome_consul") + + for action_log in temp_rc_action_logs: + senator_action_log = SenatorActionLog.objects.get(action_log=action_log) + senator_action_log.delete() + action_log.delete() + + + class Migration(migrations.Migration): dependencies = [ @@ -24,4 +77,10 @@ class Migration(migrations.Migration): old_name='notification', new_name='action_log', ), + migrations.AlterField( + model_name='title', + name='senator', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='titles', to='rorapp.senator'), + ), + migrations.RunPython(create_temp_rc_log, remove_temp_rc_log), ] diff --git a/backend/rorapp/models/senator_action_log.py b/backend/rorapp/models/senator_action_log.py index 6ffa06db..8cefa1fa 100644 --- a/backend/rorapp/models/senator_action_log.py +++ b/backend/rorapp/models/senator_action_log.py @@ -3,7 +3,7 @@ from rorapp.models.action_log import ActionLog -# Model for representing relationships between senators and action_logs +# Model for representing relationships between senators and action logs class SenatorActionLog(models.Model): senator = models.ForeignKey(Senator, on_delete=models.CASCADE) action_log = models.ForeignKey(ActionLog, on_delete=models.CASCADE) diff --git a/backend/rorapp/views/start_game.py b/backend/rorapp/views/start_game.py index efc77272..05d2e8a6 100644 --- a/backend/rorapp/views/start_game.py +++ b/backend/rorapp/views/start_game.py @@ -107,9 +107,7 @@ def start_game(self, request, game_id=None): temp_rome_consul_title = Title(name="Temporary Rome Consul", senator=senators[0], start_step=step, major_office=True) temp_rome_consul_title.save() - # Update senator ranks - rank_senators_and_factions(game.id) - + # Create action log and senator action log for temporary rome consul action_log = ActionLog( index=0, step=step, @@ -124,6 +122,9 @@ def start_game(self, request, game_id=None): ) senator_action_log.save() + # Update senator ranks + rank_senators_and_factions(game.id) + # Create potential actions for faction in factions: action = PotentialAction(