Просмотр исходного кода

[scrobbles] Remove old FK fields

Colin Powell 3 недель назад
Родитель
Сommit
eda0ae101b

+ 0 - 45
vrobbler/apps/scrobbles/admin.py

@@ -26,27 +26,6 @@ class ScrobbleInline(admin.TabularInline):
         "timezone",
         "videogame_save_data",
         "screenshot",
-        "video",
-        "podcast_episode",
-        "track",
-        "video_game",
-        "book",
-        "paper",
-        "sport_event",
-        "food",
-        "board_game",
-        "geo_location",
-        "task",
-        "mood",
-        "brick_set",
-        "trail",
-        "beer",
-        "puzzle",
-        "web_page",
-        "life_event",
-        "gpx_file",
-        "long_play_complete",
-        "long_play_seconds",
     )
     fk_name = "item"
 
@@ -133,34 +112,10 @@ class ScrobbleAdmin(admin.ModelAdmin):
         "is_paused",
         "in_progress",
         "media_type",
-        "long_play_complete",
         "source",
         "timezone",
         "user",
     )
-    exclude = (
-        "video",
-        "podcast_episode",
-        "track",
-        "sport_event",
-        "book",
-        "video_game",
-        "board_game",
-        "geo_location",
-        "puzzle",
-        "paper",
-        "food",
-        "task",
-        "mood",
-        "brick_set",
-        "trail",
-        "beer",
-        "web_page",
-        "life_event",
-        "gpx_file",
-        "long_play_complete",
-        "long_play_seconds",
-    )
     ordering = ("-timestamp",)
 
     def item_title(self, obj):

+ 135 - 4
vrobbler/apps/scrobbles/migrations/0072_migrate_scrobblablemixin_data.py

@@ -2,6 +2,13 @@ from django.db import migrations
 from django.apps import apps
 from django.contrib.contenttypes.models import ContentType
 
+def disable_fk_checks(cursor):
+    cursor.execute("PRAGMA foreign_keys = OFF;")
+
+def enable_fk_checks(cursor):
+    cursor.execute("PRAGMA foreign_keys = ON;")
+
+
 def copy_tags_raw_sql(cursor, old_table, old_id_column, new_item_id):
     # Get content_type_id for ScrobblableItem
     ct_id = ContentType.objects.get(app_label='scrobbles', model='scrobblableitem').id
@@ -35,7 +42,7 @@ def get_table_columns(cursor, table_name, vendor):
         raise RuntimeError(f"Unsupported DB vendor: {vendor}")
 
 
-def create_and_copy_data(apps, schema_editor, lookup_keys):
+def create_and_copy_data(apps, schema_editor, lookup_keys, old_to_new_mappings):
     app_label, model_name, model_id_field = lookup_keys
 
     db = schema_editor.connection
@@ -45,7 +52,6 @@ def create_and_copy_data(apps, schema_editor, lookup_keys):
 
     Model = apps.get_model(app_label, model_name)
     ScrobblableItem = apps.get_model("scrobbles", "ScrobblableItem")
-    ObjectWithGenres = apps.get_model("scrobbles", "ObjectWithGenres")
 
     table = Model._meta.db_table
     scrobble_table = "scrobbles_scrobble"
@@ -71,6 +77,9 @@ def create_and_copy_data(apps, schema_editor, lookup_keys):
     cursor.execute(f"SELECT {select_clause} FROM {table}")
     rows = cursor.fetchall()
 
+    mapping = {}
+    old_to_new_mappings[Model] = mapping
+
     # -----------------------
     # 2. Process rows
     # -----------------------
@@ -88,6 +97,8 @@ def create_and_copy_data(apps, schema_editor, lookup_keys):
         base.modified = data.get("modified")
         base.save(update_fields=["created", "modified"])
 
+        mapping[old_id] = base.id
+
         # -----------------------
         # 3. Update media table FK
         # -----------------------
@@ -121,6 +132,43 @@ def create_and_copy_data(apps, schema_editor, lookup_keys):
 
     print(f"  Finished migrating {app_label}.{model_name}")
 
+def reassign_foreign_keys(apps, schema_editor, old_to_new_mappings):
+    """
+    Detect all FKs pointing to old media tables and reassign to new ScrobblableItem.
+    """
+    print(old_to_new_mappings)
+    db = schema_editor.connection
+    cursor = db.cursor()
+
+    disable_fk_checks(cursor)
+    for model in apps.get_models():
+        table = model._meta.db_table
+        for field in model._meta.get_fields():
+            # Skip genre field
+            if getattr(field, 'name', None) in ['genre', 'tags']:
+                continue
+
+            if field.is_relation and field.many_to_one and field.related_model in old_to_new_mappings:
+                mapping = old_to_new_mappings[field.related_model]
+                fk_column = field.column
+                print(f"Updating FK {model._meta.label}.{field.name} ({table}.{fk_column})…")
+
+                for old_id, new_id in mapping.items():
+                    cursor.execute(f"UPDATE {table} SET {fk_column} = %s WHERE {fk_column} = %s",
+                                   [new_id, old_id])
+
+            # ManyToMany FKs
+            if field.many_to_many and not field.auto_created:
+                through_table = field.remote_field.through._meta.db_table
+                from_col = field.m2m_column_name()
+                to_model = field.remote_field.model
+                if to_model in old_to_new_mappings:
+                    mapping = old_to_new_mappings[to_model]
+                    print(f"Updating M2M {through_table}.{from_col} → ScrobblableItem")
+                    for old_id, new_id in mapping.items():
+                        cursor.execute(f"UPDATE {through_table} SET {field.m2m_reverse_name()} = %s WHERE {field.m2m_reverse_name()} = %s",
+                                       [new_id, old_id])
+    enable_fk_checks(cursor)
 
 def backfill_media(apps, schema_editor):
     models = (
@@ -142,9 +190,12 @@ def backfill_media(apps, schema_editor):
         ('webpages', 'WebPage', 'web_page_id'),
     )
 
+    old_to_new_mappings = {}
     for keys in models:
-        create_and_copy_data(apps, schema_editor, keys)
+        create_and_copy_data(apps, schema_editor, keys, old_to_new_mappings)
 
+    # After all media tables migrated, reassign FKs and M2Ms
+    reassign_foreign_keys(apps, schema_editor, old_to_new_mappings)
 
 class Migration(migrations.Migration):
     dependencies = [
@@ -169,5 +220,85 @@ class Migration(migrations.Migration):
     ]
 
     operations = [
-        migrations.RunPython(backfill_media, reverse_code=migrations.RunPython.noop)
+        migrations.RunPython(backfill_media, reverse_code=migrations.RunPython.noop),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="beer",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="board_game",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="book",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="brick_set",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="food",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="geo_location",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="life_event",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="long_play_complete",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="long_play_seconds",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="mood",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="paper",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="podcast_episode",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="puzzle",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="sport_event",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="task",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="track",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="trail",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="video",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="video_game",
+        ),
+        migrations.RemoveField(
+            model_name="scrobble",
+            name="web_page",
+        ),
     ]

+ 2 - 2
vrobbler/apps/scrobbles/mixins.py

@@ -139,7 +139,7 @@ class ScrobblableItem(TimeStampedModel):
         logger.info(
             "[scrobble_for_user] called",
             extra={
-                "id": self.id,
+                "item_id": self.id,
                 "media_type": self.__class__.__name__,
                 "user_id": user_id,
                 "scrobble_data": scrobble_data,
@@ -197,7 +197,7 @@ class ScrobblableMixin(TimeStampedModel):
             "[scrobble_for_user] called",
             extra={
                 "id": self.id,
-                "media_type": self.__class__.__name__,
+                "media_type": self.media_type,
                 "user_id": user_id,
                 "scrobble_data": scrobble_data,
             },

+ 10 - 61
vrobbler/apps/scrobbles/models.py

@@ -574,42 +574,6 @@ class Scrobble(TimeStampedModel):
             return None
         return self.item.get_concrete_instance()
 
-    # Deprecated
-    video = models.ForeignKey(Video, on_delete=models.DO_NOTHING, **BNULL)
-    track = models.ForeignKey(Track, on_delete=models.DO_NOTHING, **BNULL)
-    podcast_episode = models.ForeignKey(
-        PodcastEpisode, on_delete=models.DO_NOTHING, **BNULL
-    )
-    sport_event = models.ForeignKey(
-        SportEvent, on_delete=models.DO_NOTHING, **BNULL
-    )
-    book = models.ForeignKey(Book, on_delete=models.DO_NOTHING, **BNULL)
-    paper = models.ForeignKey(Paper, on_delete=models.DO_NOTHING, **BNULL)
-    video_game = models.ForeignKey(
-        VideoGame, on_delete=models.DO_NOTHING, **BNULL
-    )
-    board_game = models.ForeignKey(
-        BoardGame, on_delete=models.DO_NOTHING, **BNULL
-    )
-    geo_location = models.ForeignKey(
-        GeoLocation, on_delete=models.DO_NOTHING, **BNULL
-    )
-    beer = models.ForeignKey(Beer, on_delete=models.DO_NOTHING, **BNULL)
-    puzzle = models.ForeignKey(Puzzle, on_delete=models.DO_NOTHING, **BNULL)
-    food = models.ForeignKey(Food, on_delete=models.DO_NOTHING, **BNULL)
-    trail = models.ForeignKey(Trail, on_delete=models.DO_NOTHING, **BNULL)
-    task = models.ForeignKey(Task, on_delete=models.DO_NOTHING, **BNULL)
-    web_page = models.ForeignKey(WebPage, on_delete=models.DO_NOTHING, **BNULL)
-    life_event = models.ForeignKey(
-        LifeEvent, on_delete=models.DO_NOTHING, **BNULL
-    )
-    mood = models.ForeignKey(Mood, on_delete=models.DO_NOTHING, **BNULL)
-    brick_set = models.ForeignKey(
-        BrickSet, on_delete=models.DO_NOTHING, **BNULL
-    )
-    long_play_seconds = models.BigIntegerField(**BNULL)
-    long_play_complete = models.BooleanField(**BNULL)
-
     class Meta:
         ordering = ["-timestamp"]
         indexes = [
@@ -698,7 +662,7 @@ class Scrobble(TimeStampedModel):
             self.uuid = uuid4()
 
         if self.item:
-            self.media_type = self.item.MEDIA_TYPE
+            self.media_type = self.item.get_media_type()
 
         if not self.timezone:
             timezone = settings.TIME_ZONE
@@ -1094,36 +1058,20 @@ class Scrobble(TimeStampedModel):
 
     @classmethod
     def create_or_update(
-        cls, media, user_id: int, scrobble_data: dict, **kwargs
+        cls, item, user_id: int, scrobble_data: dict, **kwargs
     ) -> "Scrobble":
-        key = media_class_to_foreign_key(media.__class__.__name__)
-        media_query = models.Q(**{key: media})
-        scrobble_data[key + "_id"] = media.id
         skip_in_progress_check = kwargs.get("skip_in_progress_check", False)
         read_log_page = kwargs.get("read_log_page", None)
+        mtype = item.get_media_type()
 
 
         # Find our last scrobble of this media item (track, video, etc)
-        scrobble = (
-            cls.objects.filter(
-                media_query,
-                user_id=user_id,
-            )
-            .order_by("-timestamp")
-            .first()
-        )
+        scrobble = cls.objects.filter(item=item, user_id=user_id).order_by("-timestamp").first()
         source = scrobble_data.get("source", "Vrobbler")
-        mtype = media.__class__.__name__
 
         # GeoLocations are a special case scrobble
         if mtype == constants.MediaType.GEO_LOCATION:
-            logger.warning(
-                f"[create_or_update] geoloc requires create_or_update_location"
-            )
-            scrobble = cls.create_or_update_location(
-                media, scrobble_data, user_id
-            )
-            return scrobble
+            return cls.create_or_update_location(item, scrobble_data, user_id)
 
         if not skip_in_progress_check or read_log_page:
             logger.info(
@@ -1131,7 +1079,7 @@ class Scrobble(TimeStampedModel):
                 extra={
                     "scrobble_id": scrobble.id if scrobble else None,
                     "media_type": mtype,
-                    "media_id": media.id,
+                    "item_id": item.id,
                     "scrobble_data": scrobble_data,
                 },
             )
@@ -1175,14 +1123,15 @@ class Scrobble(TimeStampedModel):
             extra={
                 "scrobble_id": scrobble.id if scrobble else None,
                 "media_type": mtype,
-                "media_id": media.id,
+                "item_id": item.id,
                 "source": source,
             },
         )
         if mtype == constants.MediaType.FOOD and not scrobble_data.get("log", {}).get("calories", None):
-            if media.calories:
-                scrobble_data["log"] = FoodLogData(calories=media.calories)
+            if item.calories:
+                scrobble_data["log"] = FoodLogData(calories=item.calories)
 
+        scrobble_data["item_id"] = item.id
         scrobble = cls.create(scrobble_data)
         return scrobble