2010-07-06 4 views
8

je travaille à travers d'un widget ManyToManyField ordonné, et ont l'aspect frontal de celui-ci travaille bien:Ordonné ManyToManyField qui peut être utilisé dans fieldsets

alt text http://i45.tinypic.com/33e79c8.png

Malheureusement, je vais avoir un beaucoup de mal à faire fonctionner le backend. La manière la plus évidente de connecter le backend est d'utiliser une table through coupée d'un modèle avec ForeignKey s des deux côtés de la relation et d'écraser la méthode de sauvegarde. Cela fonctionnerait bien, sauf qu'en raison des idiosyncrasies du contenu, il est absolument nécessaire que ce widget soit placé dans un fieldset (en utilisant la propriété ModelAdmin fieldsets), qui est apparently not possible.

Je suis à court d'idées. Aucune suggestion?

Merci!

+0

+1 pour utiliser le mot 'idiosyncrasies' dans votre question. Idée cool. Je réfléchis. –

+1

Pouvez-vous nous dire pourquoi vous utilisez un modèle 'through' au lieu d'un stock de plusieurs à plusieurs? quelles métadonnées supplémentaires stockez-vous dans cette relation? Le problème que je vois avec l'implémentation de cette fonctionnalité dans l'admin par défaut est qu'il n'y a actuellement aucun moyen d'ajouter les métadonnées supplémentaires dans la relation through simplement en choisissant les éléments d'une liste comme le suggère votre widget. – Thomas

+0

Thomas: les données de commande seraient le champ additionnel, donnant le champ "through" trois champs: un ForeignKey de chaque côté du M2M et un ordre représentant PositiveIntegerField, avec les trois champs étant uniques_together. –

Répondre

8

En ce qui concerne la façon de configurer les modèles, vous avez raison de dire qu'une table traversante avec une colonne "order" est le moyen idéal pour la représenter. Vous avez également raison de dire que Django ne vous laissera pas faire référence à cette relation dans un fieldset. L'astuce pour résoudre ce problème est de se rappeler que les noms de champs que vous spécifiez dans les champs d'un ModelAdmin ne se réfèrent pas aux champs du Model, mais aux champs du Model, que nous sommes libres passer outre à la joie de notre coeur. Avec beaucoup de nombreux champs, cela devient difficile, mais supporter avec moi:

Disons que vous essayez de représenter les concours et les concurrents qui y concourent, avec un nombre de2many ordonné entre les concours et les concurrents où la commande représente le classement des concurrents dans ce concours. Votre models.py doit ressembler à ceci:

from django.db import models 

class Contest(models.Model): 
    name = models.CharField(max_length=50) 
    # More fields here, if you like. 
    contestants = models.ManyToManyField('Contestant', through='ContestResults') 

class Contestant(models.Model): 
    name = models.CharField(max_length=50) 

class ContestResults(models.Model): 
    contest = models.ForeignKey(Contest) 
    contestant = models.ForeignKey(Contestant) 
    rank = models.IntegerField() 

Si tout va bien, ce qui est similaire à ce que vous avez affaire. Maintenant, pour l'administrateur. J'ai écrit un exemple admin.py avec beaucoup de commentaires pour expliquer ce qui se passe, mais voici un résumé pour vous aider:

Comme je n'ai pas le code pour le widget m2m ordonné que vous avez écrit, j'ai utilisé un widget fictif d'espace réservé qui hérite simplement de TextInput. L'entrée contient une liste séparée par des virgules (sans espaces) des identifiants des concurrents, et l'ordre de leur apparition dans la chaîne détermine la valeur de leur colonne "rank" dans le modèle ContestResults. Qu'est-ce qui se passe est que nous remplaçons le ModelForm par défaut pour le concours avec le notre, puis définissons un champ "résultats" à l'intérieur (nous ne pouvons pas appeler le champ "candidats", car il y aurait un conflit de nom avec le champ m2m dans le modèle). Nous remplaçons ensuite __init__(), qui est appelée lorsque le formulaire est affiché dans l'admin, afin que nous puissions récupérer tous les ContestResults qui ont déjà été définis pour le Contest et les utiliser pour remplir le widget. Nous remplaçons également save(), afin que nous puissions à leur tour récupérer les données du widget et créer les ContestResults nécessaires. Notez que par souci de simplicité cet exemple omet des choses comme la validation des données du widget, donc les choses vont se casser si vous essayez de taper quelque chose d'inattendu dans la saisie de texte. En outre, le code pour créer ContestResults est assez simpliste et pourrait être grandement amélioré.

Je devrais également ajouter que j'ai effectivement couru ce code et vérifié que cela fonctionne.

from django import forms 
from django.contrib import admin 
from models import Contest, Contestant, ContestResults 

# Generates a function that sequentially calls the two functions that were 
# passed to it 
def func_concat(old_func, new_func): 
    def function(): 
     old_func() 
     new_func() 
    return function 

# A dummy widget to be replaced with your own. 
class OrderedManyToManyWidget(forms.widgets.TextInput): 
    pass 

# A simple CharField that shows a comma-separated list of contestant IDs. 
class ResultsField(forms.CharField): 
    widget = OrderedManyToManyWidget() 

class ContestAdminForm(forms.models.ModelForm): 
    # Any fields declared here can be referred to in the "fieldsets" or 
    # "fields" of the ModelAdmin. It is crucial that our custom field does not 
    # use the same name as the m2m field field in the model ("contestants" in 
    # our example). 
    results = ResultsField() 

    # Be sure to specify your model here. 
    class Meta: 
     model = Contest 

    # Override init so we can populate the form field with the existing data. 
    def __init__(self, *args, **kwargs): 
     instance = kwargs.get('instance', None) 
     # See if we are editing an existing Contest. If not, there is nothing 
     # to be done. 
     if instance and instance.pk: 
      # Get a list of all the IDs of the contestants already specified 
      # for this contest. 
      contestants = ContestResults.objects.filter(contest=instance).order_by('rank').values_list('contestant_id', flat=True) 
      # Make them into a comma-separated string, and put them in our 
      # custom field. 
      self.base_fields['results'].initial = ','.join(map(str, contestants)) 
      # Depending on how you've written your widget, you can pass things 
      # like a list of available contestants to it here, if necessary. 
     super(ContestAdminForm, self).__init__(*args, **kwargs) 

    def save(self, *args, **kwargs): 
     # This "commit" business complicates things somewhat. When true, it 
     # means that the model instance will actually be saved and all is 
     # good. When false, save() returns an unsaved instance of the model. 
     # When save() calls are made by the Django admin, commit is pretty 
     # much invariably false, though I'm not sure why. This is a problem 
     # because when creating a new Contest instance, it needs to have been 
     # saved in the DB and have a PK, before we can create ContestResults. 
     # Fortunately, all models have a built-in method called save_m2m() 
     # which will always be executed after save(), and we can append our 
     # ContestResults-creating code to the existing same_m2m() method. 
     commit = kwargs.get('commit', True) 
     # Save the Contest and get an instance of the saved model 
     instance = super(ContestAdminForm, self).save(*args, **kwargs) 
     # This is known as a lexical closure, which means that if we store 
     # this function and execute it later on, it will execute in the same 
     # context (i.e. it will have access to the current instance and self). 
     def save_m2m(): 
      # This is really naive code and should be improved upon, 
      # especially in terms of validation, but the basic gist is to make 
      # the needed ContestResults. For now, we'll just delete any 
      # existing ContestResults for this Contest and create them anew. 
      ContestResults.objects.filter(contest=instance).delete() 
      # Make a list of (rank, contestant ID) tuples from the comma- 
      # -separated list of contestant IDs we get from the results field. 
      formdata = enumerate(map(int, self.cleaned_data['results'].split(',')), 1) 
      for rank, contestant in formdata: 
       ContestResults.objects.create(contest=instance, contestant_id=contestant, rank=rank) 
     if commit: 
      # If we're committing (fat chance), simply run the closure. 
      save_m2m() 
     else: 
      # Using a function concatenator, ensure our save_m2m closure is 
      # called after the existing save_m2m function (which will be 
      # called later on if commit is False). 
      self.save_m2m = func_concat(self.save_m2m, save_m2m) 
     # Return the instance like a good save() method. 
     return instance 

class ContestAdmin(admin.ModelAdmin): 
    # The precious fieldsets. 
    fieldsets = (
     ('Basic Info', { 
      'fields': ('name', 'results',) 
     }),) 
    # Here's where we override our form 
    form = ContestAdminForm 

admin.site.register(Contest, ContestAdmin) 

Si vous vous demandez, je l'avais rencontré ce problème moi-même sur un projet que je travaille sur, donc la plupart de ce code provient de ce projet. J'espère que tu trouves cela utile.

+0

J'apprécie la rigueur et la réflexion de la réponse, merci! Je vais creuser cela aujourd'hui et vous laisser savoir comment cela se passe. –

+0

Je suis très proche de faire fonctionner ça, merci! La grande clé était le pointeur que fieldsets faisait référence aux champs sur un formulaire, pas sur un modèle. Le seul problème que je rencontre est que le réglage de ModelForm.base_fields [field] .initial ne semble pas prendre; les données sur le terrain sont toujours peuplées par la valeur dans la base de données (contrairement à votre cas, je vais écraser la valeur initiale d'un vrai champ m2m, c'est apparemment une distinction importante.) Des suggestions sur comment je pourrais accomplir cela? django-users post, http://bit.ly/a7R3ao –

+0

Ah, je pense que je l'ai déjà rencontré dans certains formulaires, au lieu de 'ModelForm.base_fields [field] .initial', vous devez définir' ModelForm –

Questions connexes