mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-10-31 09:03:06 +00:00 
			
		
		
		
	Migrate albums and pictures to their own tables
This commit is contained in:
		
							
								
								
									
										357
									
								
								sas/migrations/0006_move_the_whole_sas.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										357
									
								
								sas/migrations/0006_move_the_whole_sas.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,357 @@ | ||||
| # Generated by Django 4.2.17 on 2025-01-22 21:53 | ||||
| import collections | ||||
| import itertools | ||||
| import logging | ||||
| from typing import TYPE_CHECKING | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.conf import settings | ||||
| from django.db import migrations, models | ||||
| from django.db.migrations.state import StateApps | ||||
|  | ||||
| import sas.models | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     import core.models | ||||
|  | ||||
| # NB : tous les commentaires sont écrits en français, | ||||
| #      parce qu'on est sur des opérations qui sont complexes, | ||||
| #      et qui sont surtout DANGEREUSES. | ||||
| #      Ici, la clarté des explications prime sur toute autre considération. | ||||
|  | ||||
|  | ||||
| def copy_albums_and_pictures(apps: StateApps, schema_editor): | ||||
|     SithFile: type[core.models.SithFile] = apps.get_model("core", "SithFile") | ||||
|     Album: type[sas.models.Album] = apps.get_model("sas", "Album") | ||||
|     Picture: type[sas.models.Picture] = apps.get_model("sas", "Picture") | ||||
|     logger = logging.getLogger("django") | ||||
|  | ||||
|     # Il y a environ 1800 albums, 257k photos et 488k identifications | ||||
|     # d'utilisateurs dans la db de prod. | ||||
|     # En supposant qu'une insertion prenne 10ms (ce qui est très optimiste), | ||||
|     # migrer tous les enregistrements de la db prendrait plus de 2h. | ||||
|     # C'est trop long. | ||||
|     # Mais d'un autre côté, j'ai pas assez confiance dans les capacités de nos | ||||
|     # machines pour charger presque un million d'objets en mémoire. | ||||
|     # Pour faire un compromis, les albums sont migrés individuellement un à un, | ||||
|     # mais tous les objets liés à ces albums | ||||
|     # (photos, groupes de vue, groupe d'édition, identification d'utilisateurs) | ||||
|     # sont migrés en tas. | ||||
|     # | ||||
|     # Ordre des opérations : | ||||
|     # 1. On migre les albums 1 à 1 (il y en a 1800, donc c'est relativement court) | ||||
|     # 2. On migre les photos par paquet de 2500 (soit ~une centaine d'opérations) | ||||
|     # 3. On migre tous les groupes de vue et tous les groupes d'édition des albums | ||||
|     # | ||||
|     # Au total, la migration devrait demander aux alentours de 2000 insertions, | ||||
|     # ce qui est un compromis acceptable entre une migration | ||||
|     # pas trop longue et une RAM pas trop surchargée. | ||||
|     # | ||||
|     # Pour ce qui est de la répartition des tables, quatre nouvelles tables | ||||
|     # sont créées : sas_album, sas_picture, | ||||
|     # sas_pictureviewgroups et sas_picture_editgroups. | ||||
|     # Tous les albums et toutes les photos qui sont dans core_sithfile | ||||
|     # vont être copiés dans ces tables. | ||||
|     # Comme les albums sont migrés un à un, ils recevront une nouvelle | ||||
|     # clef primaire. | ||||
|     # Pour les photos, en revanche, c'est beaucoup plus sûr de leur donner | ||||
|     # le même id que celui qu'il y avait dans core_sithfile. | ||||
|     # | ||||
|     # Les identifications des photos ne sont pas migrées pour l'instant. | ||||
|     # Ce qu'on va faire, c'est qu'on va changer la contrainte de clef étrangère | ||||
|     # sur la colonne des photos pour pointer vers sas_picture | ||||
|     # au lieu de core_sithfile. | ||||
|     # Cependant, pour que ça marche, | ||||
|     # il faut qu'au moment où ce changement est effectué, | ||||
|     # toutes les clefs primaires référencées existent à la fois dans | ||||
|     # les deux tables, sinon les contraintes d'intégrité ne sont pas respectées. | ||||
|     # La migration de ce fichier va donc s'occuper de créer les nouvelles tables | ||||
|     # et d'y copier les données nécessaires. | ||||
|     # Puis une deuxième migration s'occupera de changer les contraintes. | ||||
|     # Et enfin une troisième migration supprimera les anciennes données. | ||||
|     # | ||||
|     # Pavé César | ||||
|  | ||||
|     albums = SithFile.objects.filter(is_in_sas=True, is_folder=True).prefetch_related( | ||||
|         "view_groups", "edit_groups" | ||||
|     ) | ||||
|     old_albums = collections.deque( | ||||
|         albums.filter(parent_id=settings.SITH_SAS_ROOT_DIR_ID) | ||||
|     ) | ||||
|  | ||||
|     # Changement de représentation en DB. | ||||
|     # Dans l'ancien système, un fichier était dans le SAS si | ||||
|     # un fichier spécial (le SAS_ROOT) était parmi ses ancêtres. | ||||
|     # Comme maintenant les fichiers du SAS sont dans des tables à part, | ||||
|     # il ne peut plus y avoir de confusion. | ||||
|     # Les photos ont donc obligatoirement un parent (qui est un album) | ||||
|     # et les albums peuvent avoir un parent null. | ||||
|     # Un album sans parent est considéré comme se trouvant à la racine | ||||
|     # de l'arborescence. | ||||
|     # En quelque sorte, None est le nouveau SITH_SAS_ROOT_DIR_ID | ||||
|     album_id_old_to_new = {settings.SITH_SAS_ROOT_DIR_ID: None} | ||||
|  | ||||
|     logger.info(f"migrating {albums.count()} albums") | ||||
|     while len(old_albums) > 0: | ||||
|         # Comme les albums référencent leur parent, les albums doivent être migrés | ||||
|         # par ordre croissant de profondeur dans l'arborescence. | ||||
|         # Chaque album est donc pris par la gauche de la file | ||||
|         # et ses enfants ajoutés sur la droite. | ||||
|         old_album = old_albums.popleft() | ||||
|         old_albums.extend(list(albums.filter(parent=old_album))) | ||||
|         new_album = Album.objects.create( | ||||
|             parent_id=album_id_old_to_new[old_album.parent_id], | ||||
|             event_date=old_album.date.date(), | ||||
|             name=old_album.name, | ||||
|             thumbnail=(old_album.file or None), | ||||
|             is_moderated=old_album.is_moderated, | ||||
|         ) | ||||
|         # on garde un dictionnaire qui associe les id des albums dans l'ancienne table | ||||
|         # à leur id dans la nouvelle table, pour pouvoir recréer | ||||
|         # les liens de parenté entre albums | ||||
|         album_id_old_to_new[old_album.id] = new_album.id | ||||
|  | ||||
|     pictures = SithFile.objects.filter(is_in_sas=True, is_folder=False) | ||||
|     nb_pictures = pictures.count() | ||||
|     logger.info(f"migrating {nb_pictures} pictures") | ||||
|     for i, pictures_batch in enumerate(itertools.batched(pictures, 2500), start=1): | ||||
|         Picture.objects.bulk_create( | ||||
|             [ | ||||
|                 Picture( | ||||
|                     id=p.id, | ||||
|                     name=p.name, | ||||
|                     parent_id=album_id_old_to_new[p.parent_id], | ||||
|                     thumbnail=p.thumbnail, | ||||
|                     compressed=p.compressed, | ||||
|                     original=p.file, | ||||
|                     owner_id=p.owner_id, | ||||
|                     created_at=p.date, | ||||
|                     is_moderated=p.is_moderated, | ||||
|                     asked_for_removal=p.asked_for_removal, | ||||
|                     moderator_id=p.moderator_id, | ||||
|                 ) | ||||
|                 for p in pictures_batch | ||||
|             ] | ||||
|         ) | ||||
|         logger.info(f"Migrated {min(i * 2500, nb_pictures)} / {nb_pictures} pictures") | ||||
|  | ||||
|     logger.info("Migrating album groups") | ||||
|     albums = SithFile.objects.filter(is_in_sas=True, is_folder=True).exclude( | ||||
|         id=settings.SITH_SAS_ROOT_DIR_ID | ||||
|     ) | ||||
|     Album.edit_groups.through.objects.bulk_create( | ||||
|         [ | ||||
|             Album.view_groups.through( | ||||
|                 album=album_id_old_to_new[g.sithfile_id], group_id=g.group_id | ||||
|             ) | ||||
|             for g in SithFile.view_groups.through.objects.filter(sithfile__in=albums) | ||||
|         ] | ||||
|     ) | ||||
|     Album.edit_groups.through.objects.bulk_create( | ||||
|         [ | ||||
|             Album.view_groups.through( | ||||
|                 album=album_id_old_to_new[g.sithfile_id], group_id=g.group_id | ||||
|             ) | ||||
|             for g in SithFile.view_groups.through.objects.filter(sithfile__in=albums) | ||||
|         ] | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|     dependencies = [ | ||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||
|         ("core", "0044_alter_userban_options"), | ||||
|         ("sas", "0005_alter_sasfile_options"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         # les relations et les demandes de modération étaient liées à SithFile, | ||||
|         # via le model proxy Picture. | ||||
|         # Pour que la migration marche malgré la disparition du modèle Proxy, | ||||
|         # on change la relation pour qu'elle pointe directement vers SithFile | ||||
|         migrations.AlterField( | ||||
|             model_name="peoplepicturerelation", | ||||
|             name="picture", | ||||
|             field=models.ForeignKey( | ||||
|                 on_delete=django.db.models.deletion.CASCADE, | ||||
|                 related_name="people", | ||||
|                 to="core.sithfile", | ||||
|                 verbose_name="picture", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="picturemoderationrequest", | ||||
|             name="picture", | ||||
|             field=models.ForeignKey( | ||||
|                 on_delete=django.db.models.deletion.CASCADE, | ||||
|                 related_name="moderation_requests", | ||||
|                 to="core.sithfile", | ||||
|                 verbose_name="Picture", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.DeleteModel(name="Album"), | ||||
|         migrations.DeleteModel(name="Picture"), | ||||
|         migrations.DeleteModel(name="SasFile"), | ||||
|         migrations.CreateModel( | ||||
|             name="Album", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "id", | ||||
|                     models.AutoField( | ||||
|                         auto_created=True, | ||||
|                         primary_key=True, | ||||
|                         serialize=False, | ||||
|                         verbose_name="ID", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "thumbnail", | ||||
|                     models.FileField( | ||||
|                         max_length=256, | ||||
|                         upload_to=sas.models.get_thumbnail_directory, | ||||
|                         verbose_name="thumbnail", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("name", models.CharField(max_length=100, verbose_name="name")), | ||||
|                 ( | ||||
|                     "event_date", | ||||
|                     models.DateField( | ||||
|                         default=django.utils.timezone.localdate, | ||||
|                         help_text="The date on which the photos in this album were taken", | ||||
|                         verbose_name="event date", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "is_moderated", | ||||
|                     models.BooleanField(default=False, verbose_name="is moderated"), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "edit_groups", | ||||
|                     models.ManyToManyField( | ||||
|                         related_name="editable_albums", | ||||
|                         to="core.group", | ||||
|                         verbose_name="edit groups", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "parent", | ||||
|                     models.ForeignKey( | ||||
|                         blank=True, | ||||
|                         null=True, | ||||
|                         on_delete=django.db.models.deletion.CASCADE, | ||||
|                         related_name="children", | ||||
|                         to="sas.album", | ||||
|                         verbose_name="parent", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "view_groups", | ||||
|                     models.ManyToManyField( | ||||
|                         related_name="viewable_albums", | ||||
|                         to="core.group", | ||||
|                         verbose_name="view groups", | ||||
|                     ), | ||||
|                 ), | ||||
|             ], | ||||
|             options={"verbose_name": "album"}, | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name="Picture", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "id", | ||||
|                     models.AutoField( | ||||
|                         auto_created=True, | ||||
|                         primary_key=True, | ||||
|                         serialize=False, | ||||
|                         verbose_name="ID", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "thumbnail", | ||||
|                     models.FileField( | ||||
|                         unique=True, | ||||
|                         upload_to=sas.models.get_thumbnail_directory, | ||||
|                         verbose_name="thumbnail", | ||||
|                         max_length=256, | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("name", models.CharField(max_length=256, verbose_name="file name")), | ||||
|                 ( | ||||
|                     "original", | ||||
|                     models.FileField( | ||||
|                         unique=True, | ||||
|                         upload_to=sas.models.get_directory, | ||||
|                         verbose_name="original image", | ||||
|                         max_length=256, | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "compressed", | ||||
|                     models.FileField( | ||||
|                         unique=True, | ||||
|                         upload_to=sas.models.get_compressed_directory, | ||||
|                         verbose_name="compressed image", | ||||
|                         max_length=256, | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("created_at", models.DateTimeField(default=django.utils.timezone.now)), | ||||
|                 ( | ||||
|                     "is_moderated", | ||||
|                     models.BooleanField(default=False, verbose_name="is moderated"), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "asked_for_removal", | ||||
|                     models.BooleanField( | ||||
|                         default=False, verbose_name="asked for removal" | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "moderator", | ||||
|                     models.ForeignKey( | ||||
|                         blank=True, | ||||
|                         null=True, | ||||
|                         on_delete=django.db.models.deletion.SET_NULL, | ||||
|                         related_name="moderated_pictures", | ||||
|                         to=settings.AUTH_USER_MODEL, | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "owner", | ||||
|                     models.ForeignKey( | ||||
|                         on_delete=django.db.models.deletion.PROTECT, | ||||
|                         related_name="owned_pictures", | ||||
|                         to=settings.AUTH_USER_MODEL, | ||||
|                         verbose_name="owner", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "parent", | ||||
|                     models.ForeignKey( | ||||
|                         on_delete=django.db.models.deletion.CASCADE, | ||||
|                         related_name="pictures", | ||||
|                         to="sas.album", | ||||
|                         verbose_name="album", | ||||
|                     ), | ||||
|                 ), | ||||
|             ], | ||||
|             options={"abstract": False, "verbose_name": "picture"}, | ||||
|         ), | ||||
|         migrations.AddConstraint( | ||||
|             model_name="picture", | ||||
|             constraint=models.UniqueConstraint( | ||||
|                 fields=("name", "parent"), name="sas_picture_unique_per_album" | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddConstraint( | ||||
|             model_name="album", | ||||
|             constraint=models.UniqueConstraint( | ||||
|                 fields=("name", "parent"), name="unique_album_name_if_same_parent" | ||||
|             ), | ||||
|         ), | ||||
|         migrations.RunPython( | ||||
|             copy_albums_and_pictures, | ||||
|             reverse_code=migrations.RunPython.noop, | ||||
|             elidable=True, | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,31 @@ | ||||
| # Generated by Django 4.2.17 on 2025-01-25 23:50 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|     dependencies = [("sas", "0006_move_the_whole_sas")] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="peoplepicturerelation", | ||||
|             name="picture", | ||||
|             field=models.ForeignKey( | ||||
|                 on_delete=django.db.models.deletion.CASCADE, | ||||
|                 related_name="people", | ||||
|                 to="sas.picture", | ||||
|                 verbose_name="picture", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="picturemoderationrequest", | ||||
|             name="picture", | ||||
|             field=models.ForeignKey( | ||||
|                 on_delete=django.db.models.deletion.CASCADE, | ||||
|                 related_name="moderation_requests", | ||||
|                 to="sas.picture", | ||||
|                 verbose_name="Picture", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
		Reference in New Issue
	
	Block a user