Repository: python-discord/site

Issue:
Django: Writing a migration that turns infractions into Nominations.
@jchristgit will write a migration to turn all "infraction nominations" into real Nomination objects. This has to be done after @scragly finishes https://github.com/python-discord/organisation/issues/134


Discussion:

Resolving patch:
diff --git a/pydis_site/apps/api/migrations/0044_migrate_nominations_from_infraction_to_nomination_model.py b/pydis_site/apps/api/migrations/0044_migrate_nominations_from_infraction_to_nomination_model.py
new file mode 100644
--- /dev/null
+++ b/pydis_site/apps/api/migrations/0044_migrate_nominations_from_infraction_to_nomination_model.py
@@ -0,0 +1,64 @@
+# Generated by Django 2.2.5 on 2019-09-30 12:15
+import logging
+
+from django.db import migrations
+from django.db.models import Q
+
+log = logging.getLogger('nomination_migration')
+
+
+def migrate_nominations_to_new_model(apps, schema_editor):
+    """
+    Migrations nominations from the infraction model to the nomination model.
+
+    This migration works by replaying the nomination history in chronological order, adding and
+    ending nominations as we've recorded them.
+    """
+    Infraction = apps.get_model('api', 'Infraction')
+    Nomination = apps.get_model('api', 'Nomination')
+
+    all_nominations = (
+        Q(reason__startswith="Helper nomination:") | Q(reason__startswith="Unwatched (talent-pool):")
+    )
+
+    for infraction in Infraction.objects.filter(all_nominations).order_by('inserted_at'):
+        if infraction.reason.startswith("Helper nomination:"):
+            if Nomination.objects.filter(user=infraction.user, active=True).exists():
+                log.error(
+                    f"User `{infraction.user.id}` already has an active nomination, aborting."
+                )
+                continue
+            nomination = Nomination(
+                user=infraction.user,
+                inserted_at=infraction.inserted_at,
+                reason=infraction.reason[19:],  # Strip "Helper nomination: " prefix
+                actor=infraction.actor,
+                active=True,
+            )
+            nomination.save()
+            infraction.delete()
+        elif infraction.reason.startswith("Unwatched (talent-pool):"):
+            if not Nomination.objects.filter(user=infraction.user, active=True).exists():
+                log.error(
+                    f"User `{infraction.user.id}` has no active nomination, can't end it!"
+                )
+                continue
+            nomination = Nomination.objects.get(user=infraction.user, active=True)
+            nomination.end_reason = infraction.reason[25:]  # Strip "Unwatched (talent-pool):"
+            nomination.ended_at = infraction.inserted_at
+            nomination.active = False
+            nomination.save()
+            infraction.delete()
+        else:
+            log.error(f"I don't understand this infraction: {infraction}")
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('api', '0043_infraction_hidden_warnings_to_notes'),
+    ]
+
+    operations = [
+        migrations.RunPython(migrate_nominations_to_new_model),
+    ]
diff --git a/pydis_site/apps/api/models/bot/nomination.py b/pydis_site/apps/api/models/bot/nomination.py
--- a/pydis_site/apps/api/models/bot/nomination.py
+++ b/pydis_site/apps/api/models/bot/nomination.py
@@ -39,3 +39,8 @@ class Nomination(ModelReprMixin, models.Model):
         help_text="When the nomination was ended.",
         null=True
     )
+
+    def __str__(self):
+        """Representation that makes the target and state of the nomination immediately evident."""
+        status = "active" if self.active else "ended"
+        return f"Nomination of {self.user} ({status})"


Test patch:
diff --git a/pydis_site/apps/api/tests/test_models.py b/pydis_site/apps/api/tests/test_models.py
--- a/pydis_site/apps/api/tests/test_models.py
+++ b/pydis_site/apps/api/tests/test_models.py
@@ -3,11 +3,18 @@
 from django.test import SimpleTestCase
 
 from ..models import (
-    DeletedMessage, DocumentationLink,
-    Infraction, Message,
-    MessageDeletionContext, ModelReprMixin,
-    OffTopicChannelName, Reminder,
-    Role, Tag, User
+    DeletedMessage,
+    DocumentationLink,
+    Infraction,
+    Message,
+    MessageDeletionContext,
+    ModelReprMixin,
+    Nomination,
+    OffTopicChannelName,
+    Reminder,
+    Role,
+    Tag,
+    User
 )
 
 
@@ -27,6 +34,19 @@ def test_shows_attributes(self):
 
 class StringDunderMethodTests(SimpleTestCase):
     def setUp(self):
+        self.nomination = Nomination(
+            id=123,
+            actor=User(
+                id=9876, name='Mr. Hemlock',
+                discriminator=6666, avatar_hash=None
+            ),
+            user=User(
+                id=9876, name="Hemlock's Cat",
+                discriminator=7777, avatar_hash=None
+            ),
+            reason="He purrrrs like the best!",
+        )
+
         self.objects = (
             DeletedMessage(
                 id=45,
@@ -102,3 +122,9 @@ def setUp(self):
     def test_returns_string(self):
         for instance in self.objects:
             self.assertIsInstance(str(instance), str)
+
+    def test_nomination_str_representation(self):
+        self.assertEqual(
+            "Nomination of Hemlock's Cat#7777 (active)",
+            str(self.nomination)
+        )