Browse Source

Cleaning up a bunch of changes

Colin Powell 2 years ago
parent
commit
28254f076e

+ 3 - 0
emus/settings.py

@@ -69,6 +69,7 @@ INSTALLED_APPS = [
     "allauth",
     "allauth.account",
     "django_celery_results",
+    "simple_history",
 ]
 
 SITE_ID = 1
@@ -82,6 +83,7 @@ MIDDLEWARE = [
     "django.contrib.messages.middleware.MessageMiddleware",
     "django.middleware.clickjacking.XFrameOptionsMiddleware",
     "django.middleware.gzip.GZipMiddleware",
+    "simple_history.middleware.HistoryRequestMiddleware",
 ]
 
 ROOT_URLCONF = "emus.urls"
@@ -198,6 +200,7 @@ SCRAPER_FRONTEND = os.getenv("EMUS_FRONTEND", "emulationstation")
 JSON_LOGGING = os.getenv("EMUS_JSON_LOGGING", False)
 LOG_TYPE = "json" if JSON_LOGGING else "log"
 
+FEATURED_THRESHOLD = os.getenv("EMUS_FEATURED_THRESHOLD", 0.80)
 default_level = "INFO"
 if DEBUG:
     default_level = "DEBUG"

+ 2 - 1
games/admin.py

@@ -5,13 +5,14 @@ from games.models import Developer, Game, GameSystem, Genre, Publisher, GameColl
 
 class GameAdmin(admin.ModelAdmin):
     date_hierarchy = "created"
-    list_display = ("name", "game_system", "rating", "region")
+    list_display = ("name", "game_system", "rating", "region", "featured_on",)
     list_filter = (
         "undub",
         "english_patched",
         "hack",
         "region",
         "game_system",
+        "featured_on",
     )
     ordering = ("-created",)
     search_fields = [

+ 24 - 0
games/migrations/0021_remove_game_finished_on_remove_game_started_on.py

@@ -0,0 +1,24 @@
+# Generated by Django 4.1.3 on 2023-01-05 02:18
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        (
+            'games',
+            '0020_developer_uuid_game_uuid_gamecollection_uuid_and_more',
+        ),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='game',
+            name='finished_on',
+        ),
+        migrations.RemoveField(
+            model_name='game',
+            name='started_on',
+        ),
+    ]

+ 17 - 10
games/models.py

@@ -217,14 +217,6 @@ class Game(BaseModel):
         blank=True,
         null=True,
     )
-    started_on = models.DateField(
-        blank=True,
-        null=True
-    )
-    finished_on = models.DateField(
-        blank=True,
-        null=True
-    )
 
     tags = TaggableManager(blank=True)
 
@@ -282,6 +274,20 @@ class Game(BaseModel):
         if not self.rom_file:
             return ""
         rom_file = quote(self.rom_file.path)
+        if self.game_system.slug == "scummvm":
+            new_path = list()
+            try:
+                split_path = self.rom_file.path.split("/")
+                folder_name = self.rom_file.path.split("/")[-1].split(".")[0]
+                for i in split_path:
+                    if i == "scummvm":
+                        new_path.append(f"scummvm/{folder_name}")
+                    else:
+                        new_path.append(i)
+            except IndexError:
+                pass
+            if new_path:
+                rom_file = quote("/".join(new_path))
         if not os.path.exists(self.retroarch_core_path):
             logger.info(f"Missing libretro core file at {self.retroarch_core_path}")
             return f"Libretro core not found at {self.retroarch_core_path}"
@@ -344,10 +350,11 @@ class GameCollection(BaseModel):
         """Will dump this collection to a .cfg file in /tmp or
         our COLLECTIONS_DIR configured path if dryrun=False"""
 
-        file_path = f"/tmp/custom-{self.slug}.cfg"
+        collection_slug = self.slug.replace("-", "")
+        file_path = f"/tmp/custom-{collection_slug}.cfg"
         if not dryrun:
             file_path = os.path.join(
-                settings.COLLECTIONS_DIR, f"custom-{self.slug}.cfg"
+                settings.COLLECTIONS_DIR, f"custom-{collection_slug}.cfg"
             )
 
         with open(file_path, "w") as outfile:

+ 5 - 1
games/tasks.py

@@ -1,7 +1,11 @@
 from celery import shared_task
 
+# from celery.log import get_task_logger
+
 from games.utils import import_gamelist_file_to_db_for_system, skyscrape_console
 
+# logging = get_task_logger()
+
 
 @shared_task
 def update_roms(game_system_slugs: list, full_scan=False):
@@ -17,4 +21,4 @@ def update_roms(game_system_slugs: list, full_scan=False):
         import_dict["not_imported"] = [
             game.name for game in import_dict["not_imported"]
         ]
-    print(import_dict)
+    # logging.info(import_dict)

+ 35 - 11
games/utils.py

@@ -12,7 +12,14 @@ import re
 from dateutil import parser
 from django.conf import settings
 
-from .models import Developer, Game, GameSystem, Genre, Publisher, GameCollection
+from .models import (
+    Developer,
+    Game,
+    GameSystem,
+    Genre,
+    Publisher,
+    GameCollection,
+)
 
 import logging
 
@@ -33,7 +40,9 @@ def import_gamelist_file_to_db_for_system(
     imported_games = []
     not_imported_games = []
     if not file_path:
-        file_path = os.path.join(settings.ROMS_DIR, game_system_slug, "gamelist.xml")
+        file_path = os.path.join(
+            settings.ROMS_DIR, game_system_slug, "gamelist.xml"
+        )
     if not os.path.exists(file_path):
         logger.info(
             "File path for {game_system_slug} had no gamelist.xml file, run a scraper first!"
@@ -41,7 +50,9 @@ def import_gamelist_file_to_db_for_system(
         return
 
     gamelist = ET.parse(file_path)
-    game_system = GameSystem.objects.filter(retropie_slug=game_system_slug).first()
+    game_system = GameSystem.objects.filter(
+        retropie_slug=game_system_slug
+    ).first()
     if not game_system:
         defaults = settings.GAME_SYSTEM_DEFAULTS.get(game_system_slug, None)
         full_system_name = game_system_slug
@@ -63,7 +74,9 @@ def import_gamelist_file_to_db_for_system(
             logger.warning(
                 f"While importing {name} for {game_system}, duplicate entry found"
             )
-            print(f"While importing {name} for {game_system}, duplicate entry found")
+            print(
+                f"While importing {name} for {game_system}, duplicate entry found"
+            )
             continue
 
         if not created and not full_scan:
@@ -121,11 +134,17 @@ def import_gamelist_file_to_db_for_system(
         rating = float(rating_str) if rating_str else None
         publisher = None
         if publisher_str:
-            publisher, _created = Publisher.objects.get_or_create(name=publisher_str)
+            publisher, _created = Publisher.objects.get_or_create(
+                name=publisher_str
+            )
         developer = None
         if developer_str:
-            developer, _created = Developer.objects.get_or_create(name=developer_str)
-        release_date = parser.parse(release_date_str) if release_date_str else None
+            developer, _created = Developer.objects.get_or_create(
+                name=developer_str
+            )
+        release_date = (
+            parser.parse(release_date_str) if release_date_str else None
+        )
         description = game.find("desc").text
         screenshot_path = update_media_root_for_import(game.find("image").text)
         rom_path = update_media_root_for_import(game.find("path").text)
@@ -190,7 +209,9 @@ def export_gamelist_file_to_path_for_system(game_system_slug, file_path=None):
         ET.SubElement(game_node, "marquee").text = (
             game.marquee.path if game.marquee else ""
         )
-        ET.SubElement(game_node, "video").text = game.video.path if game.video else ""
+        ET.SubElement(game_node, "video").text = (
+            game.video.path if game.video else ""
+        )
         ET.SubElement(game_node, "rating").text = str(game.rating)
         ET.SubElement(game_node, "desc").text = game.description
         ET.SubElement(game_node, "releasedate").text = release_date_str
@@ -212,6 +233,7 @@ def skyscrape_console(game_system_slug):
     scraper_config = settings.SCRAPER_CONFIG_FILE
     scraper_binary = settings.SCRAPER_BIN_PATH
     scraper_site = settings.SCRAPER_SITE
+    scraper_frontend = settings.SCRAPER_FRONTEND
 
     # If the config file is relative, append our base dir
     if scraper_config[0] != "/":
@@ -219,7 +241,9 @@ def skyscrape_console(game_system_slug):
     if not os.path.exists(scraper_config):
         logger.info(f"Config file not found at {scraper_config}")
         return
-    logger.info(f"Scraping game info using configuration file from {scraper_config}")
+    logger.info(
+        f"Scraping game info using configuration file from {scraper_config}"
+    )
 
     scrape_output = subprocess.run(
         [
@@ -229,7 +253,7 @@ def skyscrape_console(game_system_slug):
             "-s",
             f"{scraper_site}",
             "-f",
-            "emulationstation",
+            f"{scraper_frontend}",
             "-p",
             f"{game_system_slug}",
         ],
@@ -241,7 +265,7 @@ def skyscrape_console(game_system_slug):
             "-c",
             f"{scraper_config}",
             "-f",
-            "emulationstation",
+            f"{scraper_frontend}",
             "-p",
             f"{game_system_slug}",
         ],

+ 12 - 5
games/views.py

@@ -25,17 +25,23 @@ IN_PROGRESS_STATES = [states.PENDING, states.STARTED, states.RETRY]
 
 class RecentGameList(ListView):
     model = Game
-    paginate_by = 12
-    queryset = Game.objects.order_by("-created")[:64]
+    paginate_by = 10
+    queryset = Game.objects.order_by("-created")[:70]
 
     def get_context_data(self, **kwargs):
         cached_game_id = cache.get("todays_game_id", None)
         if not cached_game_id:
             todays_game = (
-                Game.objects.filter(rating__gte=0.7, featured_on__isnull=True)
+                Game.objects.filter(rating__gte=settings.FEATURED_THRESHOLD, featured_on__isnull=True)
                 .order_by("?")
                 .first()
             )
+            featured_collection = GameCollection.objects.filter(
+                slug="emus-featured"
+            ).first()
+            if featured_collection:
+                featured_collection.games.add(todays_game)
+                featured_collection.save()
             cache.set("todays_game_id", todays_game.id, settings.FEATURED_GAME_DURATION)
             todays_game.featured_on = datetime.now()
             todays_game.save(update_fields=["featured_on"])
@@ -51,11 +57,12 @@ class RecentGameList(ListView):
 class LibraryGameList(ListView):
     template_name = "games/game_library_list.html"
     model = Game
-    paginate_by = 200
+    paginate_by = 400
+    default_sort_key = "-created"
 
     def get_context_data(self, **kwargs):
         game_system_slug = self.request.GET.get("game_system")
-        order_by = self.request.GET.get("order_by", "name")
+        order_by = self.request.GET.get("order_by", self.default_sort_key)
         if order_by[0] == "-":
             order_by = order_by[1:]
             object_list = Game.objects.order_by(F(order_by).desc(nulls_last=True))

+ 12 - 3
profiles/admin.py

@@ -1,4 +1,8 @@
-from profiles.models import UserGameProgress, UserProfile
+from profiles.models import (
+    UserGamePlaythrough,
+    UserGamePlaythroughUpdate,
+    UserProfile,
+)
 from django.contrib import admin
 
 
@@ -7,6 +11,11 @@ class UserProfileAdmin(admin.ModelAdmin):
     filter_horizontal = ("favorite_games",)
 
 
-@admin.register(UserGameProgress)
-class UserGameProgressAdmin(admin.ModelAdmin):
+@admin.register(UserGamePlaythrough)
+class UserGamePlaythroughAdmin(admin.ModelAdmin):
     raw_id_fields = ["game"]
+
+
+@admin.register(UserGamePlaythroughUpdate)
+class UserGamePlaythroughUpdateAdmin(admin.ModelAdmin):
+    ...

+ 135 - 0
profiles/migrations/0004_alter_userprofile_user_historicalusergameprogress.py

@@ -0,0 +1,135 @@
+# Generated by Django 4.1 on 2022-09-07 13:38
+
+from django.conf import settings
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import django_extensions.db.fields
+import simple_history.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        (
+            'games',
+            '0020_developer_uuid_game_uuid_gamecollection_uuid_and_more',
+        ),
+        ('profiles', '0003_alter_usergameprogress_user'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='userprofile',
+            name='user',
+            field=models.OneToOneField(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name='profile',
+                to=settings.AUTH_USER_MODEL,
+            ),
+        ),
+        migrations.CreateModel(
+            name='HistoricalUserGameProgress',
+            fields=[
+                (
+                    'id',
+                    models.BigIntegerField(
+                        auto_created=True,
+                        blank=True,
+                        db_index=True,
+                        verbose_name='ID',
+                    ),
+                ),
+                (
+                    'created',
+                    django_extensions.db.fields.CreationDateTimeField(
+                        auto_now_add=True, verbose_name='created'
+                    ),
+                ),
+                (
+                    'modified',
+                    django_extensions.db.fields.ModificationDateTimeField(
+                        auto_now=True, verbose_name='modified'
+                    ),
+                ),
+                (
+                    'started_ts',
+                    models.DateTimeField(
+                        blank=True, default=django.utils.timezone.now
+                    ),
+                ),
+                ('finished_ts', models.DateTimeField(blank=True, null=True)),
+                (
+                    'percent',
+                    models.IntegerField(
+                        default=0,
+                        help_text='Keeps track of how far through the game you are',
+                        validators=[
+                            django.core.validators.MaxValueValidator(100),
+                            django.core.validators.MinValueValidator(0),
+                        ],
+                    ),
+                ),
+                (
+                    'history_id',
+                    models.AutoField(primary_key=True, serialize=False),
+                ),
+                ('history_date', models.DateTimeField(db_index=True)),
+                (
+                    'history_change_reason',
+                    models.CharField(max_length=100, null=True),
+                ),
+                (
+                    'history_type',
+                    models.CharField(
+                        choices=[
+                            ('+', 'Created'),
+                            ('~', 'Changed'),
+                            ('-', 'Deleted'),
+                        ],
+                        max_length=1,
+                    ),
+                ),
+                (
+                    'game',
+                    models.ForeignKey(
+                        blank=True,
+                        db_constraint=False,
+                        null=True,
+                        on_delete=django.db.models.deletion.DO_NOTHING,
+                        related_name='+',
+                        to='games.game',
+                    ),
+                ),
+                (
+                    'history_user',
+                    models.ForeignKey(
+                        null=True,
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        related_name='+',
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+                (
+                    'user',
+                    models.ForeignKey(
+                        blank=True,
+                        db_constraint=False,
+                        null=True,
+                        on_delete=django.db.models.deletion.DO_NOTHING,
+                        related_name='+',
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+            ],
+            options={
+                'verbose_name': 'historical user game progress',
+                'verbose_name_plural': 'historical user game progresss',
+                'ordering': ('-history_date', '-history_id'),
+                'get_latest_by': ('history_date', 'history_id'),
+            },
+            bases=(simple_history.models.HistoricalChanges, models.Model),
+        ),
+    ]

+ 265 - 0
profiles/migrations/0005_historicalusergameplaythrough_usergameplaythrough_and_more.py

@@ -0,0 +1,265 @@
+# Generated by Django 4.1.3 on 2023-01-05 02:25
+
+from django.conf import settings
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+import django_extensions.db.fields
+import simple_history.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('games', '0021_remove_game_finished_on_remove_game_started_on'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('profiles', '0004_alter_userprofile_user_historicalusergameprogress'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='HistoricalUserGamePlaythrough',
+            fields=[
+                (
+                    'id',
+                    models.BigIntegerField(
+                        auto_created=True,
+                        blank=True,
+                        db_index=True,
+                        verbose_name='ID',
+                    ),
+                ),
+                (
+                    'created',
+                    django_extensions.db.fields.CreationDateTimeField(
+                        auto_now_add=True, verbose_name='created'
+                    ),
+                ),
+                (
+                    'modified',
+                    django_extensions.db.fields.ModificationDateTimeField(
+                        auto_now=True, verbose_name='modified'
+                    ),
+                ),
+                (
+                    'started_ts',
+                    models.DateTimeField(
+                        blank=True, default=django.utils.timezone.now
+                    ),
+                ),
+                ('finished_ts', models.DateTimeField(blank=True, null=True)),
+                (
+                    'percent',
+                    models.IntegerField(
+                        default=0,
+                        help_text='Keeps track of how far through the game you are',
+                        validators=[
+                            django.core.validators.MaxValueValidator(100),
+                            django.core.validators.MinValueValidator(0),
+                        ],
+                    ),
+                ),
+                (
+                    'history_id',
+                    models.AutoField(primary_key=True, serialize=False),
+                ),
+                ('history_date', models.DateTimeField(db_index=True)),
+                (
+                    'history_change_reason',
+                    models.CharField(max_length=100, null=True),
+                ),
+                (
+                    'history_type',
+                    models.CharField(
+                        choices=[
+                            ('+', 'Created'),
+                            ('~', 'Changed'),
+                            ('-', 'Deleted'),
+                        ],
+                        max_length=1,
+                    ),
+                ),
+                (
+                    'game',
+                    models.ForeignKey(
+                        blank=True,
+                        db_constraint=False,
+                        null=True,
+                        on_delete=django.db.models.deletion.DO_NOTHING,
+                        related_name='+',
+                        to='games.game',
+                    ),
+                ),
+                (
+                    'history_user',
+                    models.ForeignKey(
+                        null=True,
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        related_name='+',
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+                (
+                    'user',
+                    models.ForeignKey(
+                        blank=True,
+                        db_constraint=False,
+                        null=True,
+                        on_delete=django.db.models.deletion.DO_NOTHING,
+                        related_name='+',
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+            ],
+            options={
+                'verbose_name': 'historical user game playthrough',
+                'verbose_name_plural': 'historical user game playthroughs',
+                'ordering': ('-history_date', '-history_id'),
+                'get_latest_by': ('history_date', 'history_id'),
+            },
+            bases=(simple_history.models.HistoricalChanges, models.Model),
+        ),
+        migrations.CreateModel(
+            name='UserGamePlaythrough',
+            fields=[
+                (
+                    'id',
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
+                (
+                    'created',
+                    django_extensions.db.fields.CreationDateTimeField(
+                        auto_now_add=True, verbose_name='created'
+                    ),
+                ),
+                (
+                    'modified',
+                    django_extensions.db.fields.ModificationDateTimeField(
+                        auto_now=True, verbose_name='modified'
+                    ),
+                ),
+                (
+                    'started_ts',
+                    models.DateTimeField(
+                        blank=True, default=django.utils.timezone.now
+                    ),
+                ),
+                ('finished_ts', models.DateTimeField(blank=True, null=True)),
+                (
+                    'percent',
+                    models.IntegerField(
+                        default=0,
+                        help_text='Keeps track of how far through the game you are',
+                        validators=[
+                            django.core.validators.MaxValueValidator(100),
+                            django.core.validators.MinValueValidator(0),
+                        ],
+                    ),
+                ),
+                (
+                    'game',
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.DO_NOTHING,
+                        to='games.game',
+                    ),
+                ),
+                (
+                    'user',
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+            ],
+            options={
+                'ordering': ('-started_ts',),
+            },
+        ),
+        migrations.CreateModel(
+            name='UserGamePlaythroughUpdate',
+            fields=[
+                (
+                    'id',
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
+                (
+                    'created',
+                    django_extensions.db.fields.CreationDateTimeField(
+                        auto_now_add=True, verbose_name='created'
+                    ),
+                ),
+                (
+                    'modified',
+                    django_extensions.db.fields.ModificationDateTimeField(
+                        auto_now=True, verbose_name='modified'
+                    ),
+                ),
+                (
+                    'percent',
+                    models.IntegerField(
+                        default=0,
+                        help_text='Keeps track of how far through the game you are',
+                        validators=[
+                            django.core.validators.MaxValueValidator(100),
+                            django.core.validators.MinValueValidator(0),
+                        ],
+                    ),
+                ),
+                (
+                    'playthrough',
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.DO_NOTHING,
+                        to='profiles.usergameplaythrough',
+                    ),
+                ),
+                (
+                    'user',
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+            ],
+            options={
+                'get_latest_by': 'modified',
+                'abstract': False,
+            },
+        ),
+        migrations.RemoveField(
+            model_name='usergameprogress',
+            name='game',
+        ),
+        migrations.RemoveField(
+            model_name='usergameprogress',
+            name='user',
+        ),
+        migrations.AddField(
+            model_name='userprofile',
+            name='last_active',
+            field=models.DateTimeField(blank=True, null=True),
+        ),
+        migrations.DeleteModel(
+            name='HistoricalUserGameProgress',
+        ),
+        migrations.DeleteModel(
+            name='UserGameProgress',
+        ),
+        migrations.AddConstraint(
+            model_name='usergameplaythrough',
+            constraint=models.CheckConstraint(
+                check=models.Q(('finished_ts__gte', models.F('started_ts'))),
+                name='chronology',
+            ),
+        ),
+    ]

+ 54 - 3
profiles/models.py

@@ -4,12 +4,15 @@ from django.db import models
 from django.db.models.deletion import CASCADE, DO_NOTHING
 from django.utils import timezone
 from django_extensions.db.models import TimeStampedModel
+from simple_history.models import HistoricalRecords
+from django.db.models import F, Q
+
 from games.models import Game
 
 User = get_user_model()
 
 
-class UserGameProgress(TimeStampedModel):
+class UserGamePlaythrough(TimeStampedModel):
     user = models.ForeignKey(User, on_delete=models.CASCADE)
     game = models.ForeignKey(Game, on_delete=DO_NOTHING)
     started_ts = models.DateTimeField(default=timezone.now, blank=True)
@@ -19,14 +22,62 @@ class UserGameProgress(TimeStampedModel):
         validators=[MaxValueValidator(100), MinValueValidator(0)],
         help_text="Keeps track of how far through the game you are",
     )
+    history = HistoricalRecords()
+
+    def __str__(self):
+        return f"Progress in {self.game} for {self.user} ({self.percent}%)"
+
+    def create_update(self):
+        """Add an update to our playthrough"""
+        if self.progress:
+            return self.usergameplaythroughupdate_set.create(
+                user=self.user, percent=self.percent
+            )
+        return None
+
+    class Meta:
+        """Don't let playthroughs end before they start"""
+
+        constraints = [
+            models.CheckConstraint(
+                check=Q(finished_ts__gte=F("started_ts")), name="chronology"
+            )
+        ]
+        ordering = ("-started_ts",)
+
+
+class UserGamePlaythroughUpdate(TimeStampedModel):
+    user = models.ForeignKey(User, on_delete=models.CASCADE)
+    playthrough = models.ForeignKey(
+        UserGamePlaythrough, on_delete=models.DO_NOTHING
+    )
+    percent = models.IntegerField(
+        default=0,
+        validators=[MaxValueValidator(100), MinValueValidator(0)],
+        help_text="Keeps track of how far through the game you are",
+    )
 
     def __str__(self):
         return f"Progress in {self.game} for {self.user} ({self.percent}%)"
 
+    def save(self, *args, **kwargs):
+        """Can't hide from us, user was active!"""
+        self.user.user_profile.update_active_date()
+        super().save(*args, **kwargs)
+
 
 class UserProfile(models.Model):
-    user = models.OneToOneField(User, on_delete=CASCADE, related_name="profile")
-    favorite_games = models.ManyToManyField(Game, related_name="favorite_games")
+    user = models.OneToOneField(
+        User, on_delete=CASCADE, related_name="profile"
+    )
+    favorite_games = models.ManyToManyField(
+        Game, related_name="favorite_games"
+    )
+    last_active = models.DateTimeField(blank=True, null=True)
 
     def __str__(self):
         return f"User profile for {self.user}"
+
+    def update_active_date(self):
+        self.last_active = timezone.now()
+        self.save()

+ 6 - 2
templates/games/_game_table.html

@@ -8,7 +8,9 @@
         <th scope="col"><a href="?order_by={% if not '-developer' in request.get_full_path %}-{% endif %}developer">Developer</a></th>
         <th scope="col"><a href="?order_by={% if not '-publisher' in request.get_full_path %}-{% endif %}publisher">Publisher</a></th>
         <th scope="col"><a href="?order_by={% if not '-genre' in request.get_full_path %}-{% endif %}genre">Genre</a></th>
-	<th scope="col"><a href="?order_by={% if not '-release_date' in request.get_full_path %}-{% endif %}release_date">Release</a></th>
+        <th scope="col"><a href="?order_by={% if not '-release_date' in request.get_full_path %}-{% endif %}release_date">Release</a></th>
+        <th scope="col"><a href="?order_by={% if not '-featured_on' in request.get_full_path %}-{% endif %}featured_on">Featured</a></th>
+        <th scope="col"><a href="?order_by={% if not '-created' in request.get_full_path %}-{% endif %}created">Created</a></th>
         </tr>
     </thead>
     <tbody>
@@ -21,7 +23,9 @@
         <td><a href="{{game.developer.get_absolute_url}}">{{game.developer}}</a></td>
         <td><a href="{{game.publisher.get_absolute_url}}">{{game.publisher}}</a></td>
         <td style="font-size:smaller;">{% for genre in game.genre.all %}<a href="{{genre.get_absolute_url}}">{{genre}}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</td>
-	<td>{{game.release_date|date:"Y-m-d"}}</td>
+        <td>{{game.release_date|date:"Y-m-d"}}</td>
+        <td>{{game.featured_on|date:"Y-m-d"}}</td>
+        <td>{{game.created|date:"Y-m-d"}}</td>
         </tr>
         {% endfor %}
     </tbody>

+ 1 - 1
templates/games/game_library_list.html

@@ -2,7 +2,7 @@
 
 {% block page_title %}Game Library{% endblock %}
 
-{% block title %}All games by rating{% endblock %}
+{% block title %}Game library{% endblock %}
 
 {% block head_extra %}