mirror of
https://github.com/ae-utbm/sith.git
synced 2025-02-25 17:07:13 +00:00
352 lines
14 KiB
Python
352 lines
14 KiB
Python
|
# 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", "0043_bangroup_alter_group_description_alter_user_groups_and_more"),
|
||
|
("sas", "0004_picturemoderationrequest_and_more"),
|
||
|
]
|
||
|
|
||
|
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},
|
||
|
),
|
||
|
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,
|
||
|
),
|
||
|
]
|