Passer d'une FOREIGN_KEY a un CHOICES

Sans devoir demander a tout le monde de ressaisir ses données

Le problème exposé dans ce sujet a été résolu.

Bonjour,

J'utilise actuellement type = models.ForeignKey() dans l'un de mes models Django. Nous nous sommes rendu compte que celui n'évoluerai jamais mais jusque la tout le monde s'en fiche…

Mais aujourd'hui nous souhaiterions pouvoir dupliquer ce projet un nombre de fois assez important, il aurait donc était pratique que ce champs deviennent une liste de manière a ce qu'il ne soit pas nécessaire de dupliquer la DB de base sur chaque projet.

J'aurais aimer alors remplacer mon ForeignKey() par:

1
type = models.CharField(choices=MON_CHOICES)

Le seul bémol c'est que Django après la migration ne fait plus le lien pour les données déjà entrer.

Auriez vous la solution a mon problème?

Merci d'avance!

+0 -0

l'attribut queryset est fait pour toi, exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    subcategory = forms.ModelChoiceField(
        label=_(u"Sous catégories de votre tutoriel. Si aucune catégorie ne convient "
                u"n'hésitez pas à en demander une nouvelle lors de la validation !"),
        queryset=SubCategory.objects.all(),
        required=True,
        widget=forms.SelectMultiple(
            attrs={
                'required': 'required',
            }
        )
    )

Le seul bémol c'est que Django après la migration ne fait plus le lien pour les données déjà entrer.

Auriez vous la solution a mon problème?

Sanoc

En fait, ce que tu essaye de faire est un peu spécial. Si tu veux absolument passer ton foreign key vers du choices, après la génération de tes migrations de manière automatique par django, tu dois repasser sur ton fichier de migration pour le modifier à la main, et ainsi lui dire que toutes pour tes données actuelles, lire ton ancien modèle et associer le bon choices.

Tu peux t'aider de la doc django pour écrire/modifier tes migrations.

Si tu peux fournir ton ancien et nouveau modèle, on peut t'aider à écrire ton fichier de migration.

Artragis, cela ne résoud pas mon problème puisque je devrais toujours copier les données de ma DB a chaque nouveau projet. De plus ce que tu me présente la ressemble plus a un formulaire si je ne dit pas de bétise.

Dans tout les cas je pense que firm1 a mis le doigt sur quelque chose d'intéressant effectivement. Je vais essayer de pousser de ce côté et je te vous tient au courant dès que sa avance.

En fait, ce que tu essaye de faire est un peu spécial. Si tu veux absolument passer ton foreign key vers du choices, après la génération de tes migrations de manière automatique par django, tu dois repasser sur ton fichier de migration pour le modifier à la main, et ainsi lui dire que toutes pour tes données actuelles, lire ton ancien modèle et associer le bon choices.

Tu peux t'aider de la doc django pour écrire/modifier tes migrations.

Si tu peux fournir ton ancien et nouveau modèle, on peut t'aider à écrire ton fichier de migration.

firm1

Pour toi il existerai une autre solution moins spécial? Parce que sinon je suis ouvert a d'autre bonne pratique! ;)

+0 -0

Pour toi il existerai une autre solution moins spécial? Parce que sinon je suis ouvert a d'autre bonne pratique! ;)

Rien de "moins spécial" c'est tout simplement que ce n'est pas courant de voir quelqu'un migrer le contenu de sa table dans une liste d’élément de type choices.

L'autre façon de résoudre ton problème serait de garder ton modèle et d'exporter le contenu de tes données sous forme de fixtures en json avec dumpdata. Lorsque du dupliquera ton projet tu devras lancer la première fois le chargement de tes données/fixtures initiales pour remplir les tables qui contiennent tes données référentielles (car oui, il s'agit de données réferentielles).

Ce n'est pas grave artragis, ça arrive a tout le monde ;)

Quand a l'idée de fixture de firm1, je préferai mettre ça vraiment au propre vu que le projet va passer dans les mains de grand messieurs avec beaucoup d'intervenant qui vont retoucher le code dans le futur.

Dans tout les cas je commence ça d'ici quelques heure, merci encore =)

+0 -0

Alors attention les yeux me revoilà:

Ancien models.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#The type of element in the business canvas
class BusinessCanvasType(models.Model):
    class Meta:
        verbose_name = _('Business canvas type')

    name = models.CharField(max_length=50)

    def __str__(self):
        return self.name

#Element of the business canvas
#Can be disactivated if it's archived
class BusinessCanvasElement(models.Model):
    class Meta:
        verbose_name = _('Business canvas element')

    title = models.CharField(max_length=200)
    comment = models.TextField(blank=True,max_length=2000)
    #Created on
    date = models.DateTimeField(auto_now_add=True, auto_now=False,)
    type = models.ForeignKey(BusinessCanvasType,verbose_name=_('Type'))
    company = models.ForeignKey(Company)
    #True -> Archived | False -> Current use
    disactivated = models.BooleanField(default=False, verbose_name=_('Disactivated'))

    def __str__(self):
        return self.title

Nouveau models.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
BUSINESS_CANVAS_TYPE_CHOICES = (
    (1, 'KeyPartner'),
    (2, 'KeyActivitie'),
    (3, 'ValueProposition'),
    (4, 'CustomerRelationship'),
    (5, 'KeyResource'),
    (6, 'Channel'),
    (7, 'CustomerSegment'),
    (8, 'CostStructure'),
    (9, 'RevenueStream'),
    (10, 'BrainstormingSpace'),
)

#Element of the business canvas
#Can be disactivated if it's archived
class BusinessCanvasElement(models.Model):
    class Meta:
        verbose_name = _('Business canvas element')

    title = models.CharField(max_length=200)
    comment = models.TextField(blank=True,max_length=2000)
    #Created on
    date = models.DateTimeField(auto_now_add=True, auto_now=False,)
    type = models.CharField(max_length=20, choices=BUSINESS_CANVAS_TYPE_CHOICES)
    company = models.ForeignKey(Company)
    #True -> Archived | False -> Current use
    disactivated = models.BooleanField(default=False, verbose_name=_('Disactivated'))

    def __str__(self):
        return self.title

Migration autogénéré

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Migration(migrations.Migration):

    dependencies = [
        ('businessCanvas', '0002_auto_20150604_1022'),
    ]

    operations = [
        migrations.AlterField(
            model_name='businesscanvaselement',
            name='type',
            field=models.CharField(max_length=20, choices=[(1, b'KeyPartner'), (2, b'KeyActivitie'), (3, b'ValueProposition'), (4, b'CustomerRelationship'), (5, b'KeyResource'), (6, b'Channel'), (7, b'CustomerSegment'), (8, b'CostStructure'), (9, b'RevenueStream'), (10, b'BrainstormingSpace')]),
        ),
    ]

J'imagine qu'il faut donc que j'autopopule mon nouveau champs un peu comme dans la documentation que tu m'a fournis, mais comment faire le lien entre nouveau et ancien champs? devrais-je:

  • Creer un nouveau champ en parallèle de l'ancien
  • Autopopuler le nouveau sur la base de l'ancien
  • Supprimer l'ancien champ

Dans ce cas je pourrais le faire avec une fonction de ce genre:

1
2
3
4
5
def gen_uuid(apps, schema_editor):
    MyModel = apps.get_model('BusinessCanvas', 'BusinessCanvasElement')
    for row in MyModel.objects.all():
        row.nouveauChamp = row.type.id
        row.save()

Je suis sur la bonne route?

+0 -0

Alors voici comment j'ai procédé:

Migration N°1

J'ajoute le nouveau champ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Migration(migrations.Migration):

    dependencies = [
        ('businessCanvas', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='businesscanvaselement',
            name='newtype',
            field=models.CharField(max_length=20, null=True, choices=[(1, b'KeyPartner'), (2, b'KeyActivitie'), (3, b'ValueProposition'), (4, b'CustomerRelationship'), (5, b'KeyResource'), (6, b'Channel'), (7, b'CustomerSegment'), (8, b'CostStructure'), (9, b'RevenueStream'), (10, b'BrainstormingSpace')]),
        ),
    ]

Migration N°2

Populate mon nouveau champ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Migration(migrations.Migration):

    def gen_newtype(apps, schema_editor):
        MyModel = apps.get_model('businessCanvas', 'BusinessCanvasElement')
        MySecondModel = apps.get_model('businessCanvas', 'BusinessCanvasType')
        for row in MyModel.objects.all():
            id = row.type_id
            for row2 in MySecondModel.objects.all():
                if(row2.id == id):
                    row.newtype = row2.name
                    row.save()

    dependencies = [
        ('businessCanvas', '0002_businesscanvaselement_newtype'),
    ]

    operations = [
        migrations.RunPython(gen_newtype, reverse_code=migrations.RunPython.noop),
    ]

Migration N°3

Suppression du null=True

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Migration(migrations.Migration):

    dependencies = [
        ('businessCanvas', '0003_auto_20150604_1253'),
    ]

    operations = [
        migrations.AlterField(
            model_name='businesscanvaselement',
            name='newtype',
            field=models.CharField(max_length=20, choices=[(1, b'KeyPartner'), (2, b'KeyActivitie'), (3, b'ValueProposition'), (4, b'CustomerRelationship'), (5, b'KeyResource'), (6, b'Channel'), (7, b'CustomerSegment'), (8, b'CostStructure'), (9, b'RevenueStream'), (10, b'BrainstormingSpace')]),
        ),
    ]

Migration N°4

Suppression de l'ancien champ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Migration(migrations.Migration):

    dependencies = [
        ('businessCanvas', '0004_auto_20150604_1257'),
    ]

    operations = [
        migrations.RemoveField(
            model_name='businesscanvaselement',
            name='type',
        ),
    ]

Migration N°5

Rename le nouveau champ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Migration(migrations.Migration):

    dependencies = [
        ('businessCanvas', '0005_auto_20150604_1304'),
    ]

    operations = [
        migrations.RenameField(
            model_name='businesscanvaselement',
            old_name='newtype',
            new_name='type',
        ),
    ]

Ancienne DB:

Textes complets | id | title | comment | date | disactivated | company_id | type

Nouvelle DB:

Textes complets | id | title | comment | date | disactivated | company_id | type

A la différence prêt que type contient le nom et non plus l'ID de ma FOREIGNKEY.

Y aurait-il moyen de faire comprendre a Django que je veut qu'il stocke l'integer mais tout en gardant la possibilité de retrouver le name associé?

Mieux encore:

Y aurait-il moyen de pouvair faire appel a ce name associé tout comme je le faisait avec ma table de DB? de manière a ne pas avoir a refactorer toutes mes pages.

+0 -0

Y aurait-il moyen de faire comprendre a Django que je veut qu'il stocke l'integer mais tout en gardant la possibilité de retrouver le name associé?

Si j'en crois ta migration N°2 avec la ligne row.newtype = row2.name, tu demandes de stocker le name. Mais tu pourrais aussi lui demander de stocker l'ID dans ton tuple BUSINESS_CANVAS_TYPE_CHOICES en vérifiant si le name de ta BD correspond à l'un des labels de ton tuple.

Ainsi tu rentrerais dans les standard django sans avoir besoin de refaire tes pages, etc.

J'y ai penser, mais comment retrouver le name avec l'ID mis en DB?

1
choices=[(1, b'KeyPartner'), ])

Je ferais object.type.name, mais comment nommer un champs de mon choices 'name'?

+0 -0

Tu ne pourrais pas t'en sortir avec quelques dans ce gout là (je t'ai mis des commentaires pour expliquer) ?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
BUSINESS_CANVAS_TYPE_CHOICES = (
    (1, 'KeyPartner'),
    (2, 'KeyActivitie'),
    (3, 'ValueProposition'),
    (4, 'CustomerRelationship'),
    (5, 'KeyResource'),
    (6, 'Channel'),
    (7, 'CustomerSegment'),
    (8, 'CostStructure'),
    (9, 'RevenueStream'),
    (10, 'BrainstormingSpace'),
)

class Migration(migrations.Migration):

    def gen_newtype(apps, schema_editor):
        MyModel = apps.get_model('businessCanvas', 'BusinessCanvasElement')
        for row in MyModel.objects.all():
            old_name = row.type.name # on récupère le nom correspondant dans la table d'avant
            for tp in BUSINESS_CANVAS_TYPE_CHOICES: # parcours la liste de tuples pour matcher les noms entre eux
                if(tp[1] == old_name): # si le nom dans la table correspond à l'un des noms de la liste des choix 
                    row.newtype = tp[0] # on mets à jour uniquement avec l'id du tuple
                    row.save()
                    break
            row.newtype = 10 # valeur par défaut au cas ou le nom n'a pas été trouvé dans la liste de choix

    dependencies = [
        ('businessCanvas', '0002_businesscanvaselement_newtype'),
    ]

    operations = [
        migrations.RunPython(gen_newtype, reverse_code=migrations.RunPython.noop),
    ]

Si, cela a marcher lorsque je l'ai essayer au début. Mais le problème et que le code existant lui contient des appels du type:

1
2
element = BusissCanvasElement.objects.get(id=..)
nom = element.type.name

hors le element.type.name ne répond plus car ma liste de choix BUSINESS_CANVAS_TYPE_CHOICES n'a pas de name

Dans le cas présent je n'avais que quelques petits appel, j'ai donc modifié a la main et tout marche bien. Mais j'aurais aimer trouver la solution pour d'autre partie plus grande demandant le même refactoring.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte