Create a model with two foreign keys on a post save signal

Hello everyone,
I hope that the title of my message will be precise enough, if it is not the case I apologize in advance.
I do not know how to do the following thing:
I have two models:

the first one named Band

class Band(models.Model):
    ''' Model to manage band '''

    
    name = models.CharField('Nom du Groupe',
                            max_length=80,
                            default='En cours de création',
                            unique=True,

                            )

    bio = models.TextField("Courte description",
                           max_length=500,
                           blank=True)

    town = models.CharField("Ville", max_length=60, blank=True)
    avatar = models.ImageField(null=True, blank=True, upload_to='band_avatar/')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    owner = models.OneToOneField(User, on_delete=models.PROTECT)
    

and the second one witch provides the members of the Band

class Member(models.Model):
    ''' Musicians of the band '''
    member = models.ForeignKey(User, on_delete=models.CASCADE)
    band = models.ForeignKey(Band, on_delete=models.CASCADE)

How can I do the following thing?
The user creating the Band model must be added automatically as a member of the band (he is also the owner of the Band , cf : owner field).
I wonder if I should not add signals on the post save of the model Band? Like this :

class Member(models.Model):
    ''' Musicians of the band '''
    member = models.ForeignKey(User, on_delete=models.CASCADE)
    band = models.ForeignKey(Band, on_delete=models.CASCADE)

    @receiver(post_save, sender=Band)
    def create_member(sender, instance, created, **kwargs):
        if created:
            Member.objects.create(member=instance.owner, band=instance)

    @receiver(post_save, sender=Band)
    def save_member(sender, instance, **kwargs):
        # some stuff
        

But I am disappointed by the use of the two foreign keys of my association table.
I thank you in advance for your lights!

Hi @Aurelia_Gourbere

Your Member model is fine. Having an associative model/table is perfectly fine and it is good practice in database normalization :+1: (in fact that’s what Django do behind the scenes when you create a ManyToMany relationship).

In this case the best approach would be to create a ManyToManyField relationship in the Band model.

class Band(models.Model):
    # ...
    members = models.ManyToManyField(User)

Then you can use it like this:

freddie = User.objects.get(pk=1)

band = Band.objects.create(name='Queen', owner=freddie)
band.members.add(freddie)

You can also add several members at the same time

band.members.add(freddie, brian, roger)

The only caveat is that before you can use band.members.add(), the band instance must be saved in the database, otherwise you might get an integrity error.

Now your question about the signals, in this particular case since you have direct access to the Band model, I would avoid creating a signal for this, and rather I would override the save() method.

class Band(models.Model):
    # ...
    members = models.ManyToManyField(User)

    def save(self, *args, **kwargs):
        created = self.pk is None  # important to come before
        super().save(*args, **kwargs)
        if created:
            # automatically add the owner as a member when 
            # the band is created...
            self.members.add(self.owner)
1 Like

I thank you very much for your answer! It also allows me to better understand how Django handles ManytoManyFields relationships. I’m going to rewrite my model. I also appreciate the example of the band (Queen, Roger and Freddie!):grinning:
In a ManytoMany relationship, since we can not use on_delete Cascade, what happens if the user is deleted? How to delete the member entry? Must we do this manually with remove() method ?
Have a good day !

1 Like

If you delete a member entry (ie User instance), it will removed automatically from the ManyToManyField relationship.

If you remove a Band, the User instances associated as members won’t be deleted from the database.

Now if you simply want to remove a member from the band, without deleting anything, you can do this:

freddie = User.objects.get(pk=1)
band.members.remove(freddie)

Or if you want to remove all members at once:

band.members.clear()

Good evening,
Some news, I finally opted for an association table because according to the django doc
It seems interesting to have additional fields to ManytoMany relationship as described in the example.
However, I still wanted to add the creator of the group to the group members when creating the group.
After a few hours of refflexion I just ended up with a code that works by starting over the idea of setting up a signal when creating the Band model.

model.py

class Band(models.Model):
    ''' Model to manage band '''
	name = models.CharField('Nom du Groupe',
                            max_length=80,
                            default='En cours de création',
                            unique=True,

                            )

    bio = models.TextField("Courte description",
                           max_length=500,
                           blank=True)

    code = models.CharField("code postal", max_length=5, blank=True)
    county_name = models.CharField("Nom du département", max_length=60, blank=True)
    town = models.CharField("Ville", max_length=60, blank=True)
    avatar = models.ImageField(null=True, blank=True, upload_to='band_avatar/')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    type = models.CharField('Type', max_length=80, choices=TYPE_OF_BAND, blank=False)

    owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='owner_band')
    members = models.ManyToManyField(User, through='Membership')



class Membership(models.Model):

    musician = models.ForeignKey(User, on_delete=models.CASCADE)
    band = models.ForeignKey(Band, on_delete=models.CASCADE)
    date_joined = models.DateField(auto_now_add=True)
    invite_reason = models.CharField(max_length=64)

    @receiver(post_save, sender=Band)
    def create_first_member(sender, instance, created, **kwargs):
        if created:
            first_member = Membership(musician = instance.owner,
                                      band = instance,
                                      invite_reason = "band's fouder")
            first_member.save()

Have a good day .

1 Like

That’s great!

Now I wish Django would return a list of Membership instead of a list of User when you access the band.members.all() using the “through” attribute

Hello, You were right Vitor! I could not reach the members as memberships but only as users. So I sent in the context of my view (Detailview) a queryset of Memberships:

class BandDetailView(DetailView, LoginRequiredMixin):

    model = Band

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['sidenav_band'] = 'sidenav_band'
        members = Membership.objects.filter(band=self.object.id)
        context['members'] = members
        return context
        

and in template.html

{% for member in members %}
    <li class="collection-item avatar ">
       {% if member.musician.userprofile.avatar %}
      <img src="{{ member.musician.userprofile.avatar.url }}" alt="" class="circle">
        {% else %}
       <img src="{% static 'core/img/0.jpg' %}" alt="" class="circle">
        {% endif %}
        <span class="title">{{ member.musician.userprofile.username }}</span>
        <p> {{ member.invite_reason }} </br>
            {{ member.date_joined }}
            
1 Like

That’s a good solution!

Just one note on the LoginRequiredMixin, always place it on the leftmost position in the inheritance list, like this:

class BandDetailView(LoginRequiredMixin, DetailView):
    # ...

Because it controls the access by overriding the dispatch method, so this way you guarantee no other class/mixin will override and ignore the access control, potentially exposing a view to non authenticated users :+1:

I thank you very much for this piece of advice, Vitor! I have corrected my code . I did not use dispatch method yet … I will read some documentation about that . Have a good day!

Hi Vitor,

Hello from South Finland. :slight_smile:

I have similar problem what Aurélia had, but unfortunately I have not found solution. I have two modules “Organization” and “OrganizationMembers”. I only copy the parts that I think are relevant. Here is my code.

models.py

import uuid

from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _

from accounts.models import User

class Organization(models.Model):
    """ Main model for Organization. """
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(
        verbose_name=_('Name'),
        max_length=255,
        unique=True
    )
    code = models.CharField(
        verbose_name=_('Code'),
        max_length=255,
        null=True,
        blank=True
    )
    owner = models.ForeignKey(
        User,
        on_delete=models.PROTECT,
        verbose_name=_('Owner'),
        related_name='owner',
    )
    members = models.ManyToManyField(
        User,
        verbose_name=_('Organization members'),
        related_name='Members',
        through='OrganizationMembers',
        blank=True
    )

    class Meta:
        ordering = ['code', 'name']

    def __str__(self):
        """String for representing the Model object."""
        return f'{self.name}, {self.code}'


class OrganizationMembers(models.Model):
    """Model representing members of the Organization"""
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    organization = models.ForeignKey(
        Organization,
        on_delete=models.CASCADE,
        verbose_name=_('Organization')
    )

    member = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        verbose_name=_('Member'),
        null=True,
        blank=True
    )

    organization_admin = models.BooleanField(
        verbose_name=_('Organization admin'),
        default=False
    )

    class Meta:
        ordering = ['organization', 'member']
        unique_together = ('organization', 'member')

    def __str__(self):
        """String for representing the Model object."""
        return f'{self.member}'

    # Add organization owner to member list and set organization_admin is True,
    # when new organization will be created.
    @receiver(post_save, sender=Organization)
    def add_owner(sender, instance, created, **kwargs):
        if created:
            owner_member = OrganizationMembers(
                organization=instance,
                member=instance.owner,
                organization_admin=True,
            )
            owner_member.save()

My problem is that when I have “member=instance.owner” in post_save then new member will not added to OrganizationMembers list. If I comment it out, then post_save will add new member line with all other values, but without member. Member in OrganizationMembers module is not mandatory in testing purposes.

Thanks