Search code examples
djangomany-to-manydjango-orm

Regroup many-to-many fields in django


I have these model relationships

class ExerciseCategory(models.Model):
    owner = models.ForeignKey(get_user_model(), on_delete=models.SET_NULL, blank=True, null=True)
    name = models.CharField(max_length=100)
class UserExercises(models.Model): #Through table for the many-to-many relationship between User and Exercise
    user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name="user")
    exercise = models.ForeignKey('Exercise', on_delete=models.CASCADE)

class Exercise(models.Model):
    category= models.ManyToManyField(ExerciseCategory, through='CategoryExercisesThrough',blank=True, null=True) 
    assignee = models.ManyToManyField(get_user_model(), blank=True, null=True, through=UserExercises, related_name="assignee", through_fields=('exercise', 'user'))

class CategoryExercisesThrough(models.Model): #Through table for the many-to-many relationship between ExerciseCategory and Exercise
    category= models.ForeignKey(ExerciseCollections, on_delete=models.CASCADE)
    exercise = models.ForeignKey(Exercise, on_delete=models.CASCADE)

I want to get all the UserExercises of a given user and then display them in my template grouped by category, so if I have: {Ex1: {category: ["C1", "C2"]}, Ex2: {category: "C2"}}, I want them reordered like this: {"C1": ["Ex1"], "C2": ["Ex2", "Ex1"]}


Solution

  • I suggest to just post-process this manually, so:

    from itertools import groupby
    from operator import attrgetter
    
    qs = (
        UserExercises.objects.filter(user=request.user)
        .annotate(category_id=F('exercise__category__pk'))
        .order_by('category_id')
    )
    
    result = {k: list(vs) for k, vs in groupby(qs, attrgetter('category_id'))}

    this will map the primary key of the ExerciseCategory on to the list of UserExercisess, you can further post-process it by fetching the categories as well:

    categories = {ec.pk: ec for ec in ExerciseCategory.objects.filter(pk__in=result)}
    result = {categories[k]: v for k, v in result.items()}

    Note: normally a Django model is given a singular name, so UserExercise instead of UserExercises.


    Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.