Define an order for ManyToManyField with Django

Question:

Is there a way to define the order of objects related through a ManyToManyField?

Example:

  • ArticleContainer1 contains in this order :
    article1, article2, article3, article6

  • ArticleContainer2 contains in this order :
    article3, article2, article1, article4

  • ArticleContainer3 contains in this order :
    article5

Here are my classes :

class Article(models.Model):    
    title = models.CharField(max_length=200)

class ArticleContainer(models.Model):
   contents = models.ManyToManyField(Article, blank=True, null=True)
Asked By: Christophe Debove

||

Answers:

If you use a explicitly defined through model for m2m relationship you can add your own attribute order-id. Then You can extend ManyToManyField to populate order-id per your logic on create/update and a model m2m manager that will sort the results when you fetch them by the order-id attribute.

Answered By: enticedwanderer

See the docs about through.

Answered By: Sander van Leeuwen

So this is an example I have, a site that organizes people into departments with per department ordering. Its the same concept as your problem but with different models. This example uses many-to-many through table.

class Department(models.Model):
    slug = models.SlugField(
        verbose_name    = _(u'Slug'),
        help_text           = _(u'Uri identifier for this department.'),
        max_length=255
    )
    name = models.CharField(
        verbose_name    = _(u'Department Name'),
        help_text           = _(u'The department's name.'),
        max_length      = 255
    )
    description = models.TextField(
        verbose_name    = _(u'Description'),
        help_text           = _(u'Department's description')
    )
    people = models.ManyToManyField(
        Person,
        through             = 'DepartmentPeople',
        related_name    = 'people',
        verbose_name    = _(u'People'),
        help_text           = _(u'People in this Department')
    )
    order_by = models.IntegerField(
        verbose_name    = _(u'Ordering Weight'), 
        help_text           = _(u'This item's weight within a list.'),
        max_length      = 255
    )

    class Meta:
        verbose_name = _(u"Department")
        verbose_name_plural = _(u"Departments")
        ordering = ['order_by',]

    def people_list(self):
        return [dp.person for dp in DepartmentPeople.objects.filter(department=self).order_by('order')]

    def __unicode__(self):
        return self.name        

And the through model:

class DepartmentPeople(models.Model):
    person = models.ForeignKey(
        Person,
        verbose_name    = _(u'Person'),
        help_text           = _(u'Person is a member of this deparment.'),
    )
    department = models.ForeignKey(
        Department,
        verbose_name    = _(u'Department'),
        help_text           = _(u'Pseron is a member of this department.'),
    )       
    order = models.IntegerField(
        verbose_name    = _(u'Order'),
        help_text           = _(u'What order to display this person within the department.'),
        max_length      = 255
    )

    class Meta:
        verbose_name = _(u"Department Person")
        verbose_name_plural = _(u"Department People")
        ordering = ['order',]

    def __unicode__(self):
        return self.person.first_name + " " + self.person.last_name + " is a member of " + self.department.name + (" in position %d" % self.order)

And the admin:

class DepartmentPeopleInline(admin.TabularInline):
    model = DepartmentPeople
    extra = 1

class DepartmentAdmin(admin.ModelAdmin):
    inlines = (DepartmentPeopleInline,)

admin.site.register(Person, PersonAdmin)
admin.site.register(Department, DepartmentAdmin)

REQUEST IN COMMENT:

Note: the following is my PersonAdmin, but its needlessly complicated for this example. you could get by with a simple

class PersonAdmin(admin.ModelAdmin) :
    pass

BUT this is what i’m using in my app:

class PersonForm(forms.ModelForm):
    abstract = forms.CharField(
        widget=TinyMCE(attrs={'cols': 80, 'rows': 30})
    )

    class Meta:
        model = Person

class PersonAdmin(reversion.VersionAdmin):
    form = PersonForm
    # The Form Fieldsets
    fieldsets = [
        (
            None,
            {
                'fields'    : [('first_name', 'last_name', 'post_nominal', ), 'slug', 'title', 'headshot', 'large_photo', ('email', 'phone', ), 'abstract']
            },
        )
    ]

    # Prepopulated fields
    prepopulated_fields = {'slug': ('first_name', 'last_name', 'post_nominal', )}

    # Fields that are readonly
    #readonly_fields = ('slug', )

    def formfield_for_dbfield(self, db_field, **kwargs):
        if db_field.name == 'headshot':
            request = kwargs.pop("request", None)
            kwargs['widget'] = AdminImageWidget
            return db_field.formfield(**kwargs)
        return super(PersonAdmin, self).formfield_for_dbfield(db_field, **kwargs)
Answered By: Francis Yaconiello

On Django 2.0, the accepted answer of Francis Yaconiello works well with the exception of a warning caused by the max_length argument for the order field of class DepartmentPeople.

Django ignores max_length for integer fields, and will warn you in Django 1.8+.

I would add a unique_together entry in the Meta subclass

class Meta:
    #...
    unique_together = ['person', 'department', 'order']

to prevent data entry errors.

Answered By: jeromecc

I use that plugin: https://github.com/jazzband/django-sortedm2m
This works just fine
I had some troubles with migrations because i hadn’t read usage of the plugin, pay attention on it

Answered By: IC_

An alternative: if you just have a few items in the ManyToManyField, and you prefer not to introduce too much complexity, you could opt to add a priority to the ManyToManyField and define ordering in Meta:

class Article(models.Model):    
    title = models.CharField(max_length=200)
    priority = models.PositiveSmallIntegerField(default=16383)

class ArticleContainer(models.Model):
   contents = models.ManyToManyField(Article, blank=True, null=True)

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

A PositiveSmallIntegerField ranges from 0 to 32767. The closer to zero, the higher the field will appear.

Answered By: SaeX
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.