Mots-clé : modèle

Django annotate : à quoi ça sert ?

Je n’avais pas bien compris l’utilité de « annotate » dans les queries Django jusqu’à maintenant…

Voici quel était mon problème :

J’avais un modèle de base, Activite. Une activité contenait des choses concernant… une activité. Si si je vous jure !

Par exemple, une activité consistait à ajouter un voyage. Ou avoir une nouvelle relation. Ou faire un témoignage comme quoi on aimait bien le site. Etc.
Je voulais que ces activités soient « partageables » entre les amis.
J’ai crée un modèles ActiviteShare. Facile jusque là.

Mais lorsque je demandais à récupérer toutes les activités, il fallait que je les trie en me basant sur le fait de savoir si elles étaient partagées. Pour exagérer, imaginons une activité qui était faite en 2010. Vous étiez parti à Malibu. Génial ! Et maintenant, en 2016, un ami s’inscrit, devient votre ami, et vous voulez partager cette activité avec lui. Il faut qu’elle s’affiche dans votre mur, comme activité « partagée récemment », mais qu’elle garde sa date de 2010, ok ?

Donc je devais trier d’abord en fonction de la date de ActiviteShare puis seulement après par la date de création de l’Activite.

Seul hic : si vous aviez partagé cette activité avec 10 contacts, au moment du tri, Django renvoyait dix fois le même enregistrement.

Mon tri était super basique, du genre :

Activite.objects.filter(
    # blabla
).order_by(
    '-activiteshared__date_last_modif',
    '-date_last_modif',
    '-date_publication',)

Mais ça ne marchait pas. 🙁

La solution ? Annotate.

Pour vous expliquer avant de mettre le code : je lui demande, lors de la requête, de garder la date maximale parmi toutes les dates partagées, et de l’appeler « mx ». Ensuite, je demande de trier simplement d’abord par « mx », puis par la date de dernière modification de l’activité.

Activite.objects.filter(
    # blabla
).annotate(mx=Max('activiteshared__date_last_modif')).order_by(
    '-mx',
    '-date_last_modif',
    '-date_publication',)

Merci à ce lien qui m’a bien aidé.

Django : formulaire qui édite aussi des champs d’un modèle clé étrangère

Déclarer les modèles

Voici mes modèles en résumé, parce que vous y arriverez nécessairement un jour ou l’autre :

class Personne(models.Model):
    user = models.OneToOneField(User)
    date_naissance = models.DateField(default=None,
                                      null=True, blank=True,
                                      verbose_name=_(u'Birth date'))
    class Meta(BaseModel.Meta):
        ordering = ['date_v_debut']

Si on veut faire une forme qui édite nom, prenom et date_naissance : facile… pour date_naissance. Faire la forme avec les champs du modèle :

class ProfileForm(forms.ModelForm):
    class Meta:
        model = Personne
        fields = ('date_naissance',)

Ajoutons la date de naissance :

    a = _(u'Birthdate:')
    date_naissance = forms.DateField(
        label=a,
        widget=forms.DateInput(attrs={'title': a}))

Déclarer les champs de la clé étrangère

Etape précédente facile ! Mais pour les champs d’un modèle clé étrangère, ici User ?
D’abord l’exclure de la forme (ne vous inquiétez pas, je ne vous mets qu’une partie du code, mais vous aurez le code en entier à la fin !) : exclude = ('user',)
Ensuite mettre les champs dans __init__, ici exemple avec last_name :

    def __init__(self, *args, **kwargs):
        super(ProfileForm, self).__init__(*args, **kwargs)
        a = _(u'Last name:')
        self.fields['user_last_name'] = forms.CharField(
            label=a, max_length=100,
            widget=forms.TextInput(attrs={
                'title': a, 'size': 100, 'type': 'text',
                'placeholder': _(u'my last name'),
                'class': 'form-control'}),
            error_messages=self.e)

Donc là vous aurez tous vos champs affichés, mais : (1) ils ne seront pas remplis avec la valeur de votre modèle si vous êtes en mode édition et (2) ils ne sont pas ordonnés, car ajoutés à la fin = ils apparaîtront forcément à la fin.

Pré-remplir le champ

Voici comment pré-remplir le champ : ce n’est pas dans la déclaration de la forme, mais dans la déclaration de la vue via get_initial() :

class EditView(LoginRequiredMixin, generic.UpdateView):
    model = Personne
    template_name = 'my_home/profile/edit.html'
    form_class = ProfileForm
    success_url = reverse_lazy('my_home_index')

    def get_initial(self):
        initial = super(EditView, self).get_initial()
        a = self.object.user
        initial['user_last_name'] = a.last_name if a.last_name else u''
        return initial

Les étapes en résumé

Ce titre = principal = h1.
There’s no need to say more… 🙂

Modèles

User

Déjà fait, merci Django ! Ahahaha. Bon.

Personne

class Personne(models.Model):
    user = models.OneToOneField(User)
    date_naissance = models.DateField(default=None,
                                      null=True, blank=True,
                                      verbose_name=_(u'Birth date'))
    class Meta(BaseModel.Meta):
        ordering = ['date_v_debut']

Forme

Champs normaux + champs de la clé étrangère = du modèle « étranger » :
class ProfileForm(forms.ModelForm):

    class Meta:
        model = Personne
        fields = ('sexe', 'statut', 'est_fumeur',
                  'est_physique', 'date_naissance')
        exclude = ('user', 'est_physique')

    a = _(u'Birthdate:')
    date_naissance = forms.DateField(
        label=a,
        widget=forms.DateInput(attrs={
            'title': a,
            'class': 'form-control datetimepicker'}))

    def __init__(self, *args, **kwargs):
        super(ProfileForm, self).__init__(*args, **kwargs)
        a = _(u'Last name:')
        self.fields['user_last_name'] = forms.CharField(
            label=a, max_length=100,
            widget=forms.TextInput(attrs={
                'title': a, 'size': 100, 'type': 'text',
                'placeholder': _(u'my last name'),
                'class': 'form-control'}),
            error_messages=self.e)

Vue

Pré-remplir les champs :
class EditView(LoginRequiredMixin, generic.UpdateView):
    model = Personne
    template_name = 'my_home/profile/edit.html'
    form_class = ProfileForm
    success_url = reverse_lazy('my_home_index')

    def get_initial(self):
        initial = super(EditView, self).get_initial()
        a = self.object.user
        initial['user_last_name'] = a.last_name if a.last_name else u''
        return initial

Voilà résumé : Vue + Forme + Modèle. Inévitable sur des bons frameworks, et classique non ?

J’ai oublié une dernière chose : ces champs sont dans un OrderedDict() c’est à dire que c’est l’ordre dans lequel ont été mis les éléments qui importe, donc dans notre cas, ils seront affichés en dernier. Si jamais vous voulez les afficher en premier, vous êtes obligés de reconstruire un OrderedDict() dans lequel vous ajoutez vos champs dans l’ordre que vous désirez. Ici, je ne vais que remettre mon champ en premier, et remettre les autres.

Voici le code de __init__ final :

    def __init__(self, *args, **kwargs):
        super(ProfileForm, self).__init__(*args, **kwargs)
        field_user_first_name = forms.CharField(
            label=a, max_length=100,
            widget=forms.TextInput(attrs={
                'title': a, 'size': 100, 'type': 'text',
                'placeholder': _(u'my first name'),
                'class': 'form-control'}),
            error_messages=self.e)
        new_fields = OrderedDict([
            ('user_first_name', field_user_first_name),
        ])
        for k, v in self.fields.items():
            new_fields[k] = v
        self.fields = new_fields