Package: django-sortedone2many
sortedone2manyprovides aSortedOneToManyFieldfor django Model that establishes a
one-to-many relationship (which can also remember the order of related objects).Depends onSortedManyToManyFieldfrom the great librarydjango-sortedm2m(check it out!).Installationpip installdjango-sortedone2manyPyPI repository:https://pypi.python.org/pypi/django-sortedone2manyUsecasesSortedOneToManyrelationshipTheOneToManyrelationship has been long missing from django ORM.
A similar relationship,ManyToOne, is provided via aForeignKey,
which is always declared on the “many” side of the relationship.
In the following example (usingrelated_nameon aForeignKey):classCategory(models.Model):name=models.CharField(max_length=50)classItem(models.Model):category=ForeignKey(Category,related_name="items")item.categoryis aManyToOnerelationship, whilecategory.itemsis aOneToManyrelationship.
However, it is not easy to
manage the order of the list ofitemsin acategory.To address this need, simply add aSortedOneToManyField(from this package) to
the model on the “one” side of the relationship:classCategory(models.Model):name=models.CharField(max_length=50)items=SortedOneToManyField(Item,sorted=True,blank=True)SortedOneToManyFielduses an intermediary model with an extrasort_valuefield to manage the orders of the related objects.
It is very useful to representan ordered list of items(according to their added order or user-specified order).Also,OneToManyrelationship offers bettersemanticsandreadabilitythanForeignKey,
especially for scenarios likemaster-detailorcategory-item(where each item only belongs to one category).This blog explains it nicely.AddingOneToManyto existing modelsSinceOneToManyrelationship uses an intermediary model,
it can work without altering already-existing models/tables,
thus providing betterextensibilitythanForeignKey(which requires adding aForeignKeyfield to the model/table).
This is a big advantage when the existing models can’t be changed
(e.g., models in a third-party library, or shared among several applications).This package provides a shortcut functionadd_sorted_one2many_relationto injectOneToManyrelationship to existing models without editing the
model source code or subclassing the models.UsageAdd theSortedOneToManyFieldto the model on the “one” side of the
relationship (as opposed toForeignKeyon the “many” side):fromdjango.dbimportmodelsfromsortedone2many.fieldsimportSortedOneToManyFieldclassItem(models.Model):name=models.CharField(max_length=50)classCategory(models.Model):name=models.CharField(max_length=50)items=SortedOneToManyField(Item,sorted=True,blank=True)Here,category.itemsis the manager for relatedItemobjects (the same as
the normalManyToManyField); use it likecategory.items.add(new_item),category.items.all(). By default, the list ofitems(e.g.,category.items.all())
is sorted according to the order that eachitemis added.On the other side,item.categoryis aninstance(not manager) ofCategory(similar
to aOneToOneField); use it likeitem.category.pk,item.category = new_category.Strictly speaking,item.categoryis an instance ofsortedone2many.fields.OneToManyRelatedObjectDescriptor(a type ofpython descriptor),
which directly exposes thesinglerelated object (i.e., thecategoryinstance).
This is different from theManyRelatedObjectsDescriptor(as in the normalManyToManyField)
which exposes themanagerof thepotentially multiplerelated objects
(which is not as convenient to use in theOneToManyrelationship).SortedOneToManyFieldSimilar toSortedManyToManyField,
it uses an intermediary model that holds a ForeignKey field pointed at
the model on the “one” side of the relationship, a OneToOneField field
pointed at the model on the “many” side (to ensure the unique relationship
to the “one” side), and another field storing the
sort value (to remember to orders of the objects on the “many” side).SortedOneToManyFieldaccepts a booleansortedattribute which specifies if relationship is
ordered or not. Default is set toTrue.Refer todjango-sortedm2mfor more details.AdminFirst, add"sortedm2m"to yourINSTALLED_APPSsettings,
which provides the staticjsandcssfiles to render
the related objects in aSortedOneToManyFieldas a list of
checkboxes that can be sorted by drag’n’drop.
(That is similar to the behavior of aSortedManyToManyField).By default, aSortedOneToManyFieldis translated into a form fieldsortedone2many.forms.SortedMultipleChoiceWithDisabledFieldfor rendering.
This form field also adds a special function to the widget:
disables those checkboxes that should not be directly selected
in the current admin view (to ensure the uniqueOneToManyrelationship).E.g., in the image below, in the admin view forcategory 1,item1.categoryiscategory 2, so the checkbox foritem1is disabled
becausecategory 2has to removeitem1from itsitemslist beforecategory 1can selectitem1in the admin view.In the admin site, to display a related object on the reverse side of
aSortedOneToManyField(e.g., to displayitem1.categoryin the
admin view ofitem1), simply usesortedone2many.admin.One2ManyModelAdminas theadmin classto register your model:fromdjango.contribimportadminfromsortedone2many.adminimportOne2ManyModelAdminadmin.site.register(MyItemModel,One2ManyModelAdmin)Or, use the shortcut functionsortedone2many.admin.register:fromsortedone2many.adminimportregisterregister(MyItemModel)The related object will be rendered as a dropdown <select> list,
through which you can assign it a different value.
Two additional “change” and “add” buttons are also listed after the dropdown list
as the shortcuts to edit thecategory(similar to the appearance of aForeignKey), as shown below:Internally,One2ManyModelAdminusesOne2ManyModelFormfor rendering,
which automatically finds relatedSortedOneToManyFieldfrom the model defined in the
form’s Meta class, and add these fields to the form.
Your can subclassOne2ManyModelFormto customize it for your own model.Utility functionsUse the following helper functions insortedone2many.utilsto inject extra fields to existing models:inject_extra_field_to_model(from_model,field_name,field)add_sorted_one2many_relation(model_one,model_many,field_name_on_model_one=None,related_name_on_model_many=None)Working with existing modelsSortedOneToManyField(or generally, any extra model field) can be added to an existing model
that can’t be edited directly (e.g., in another library/app). For example, add the field to
theUsermodel indjango.contrib.auth.models.It is recommended to usedjango migrationsto do this.First, add the existing model (User) into djangomigrationsusing a migrations folderoutside the original library/app(e.g., in your own app).
This can be achieved by configuring theMIGRATION_MODULESdictionary in your djangosettings:MIGRATION_MODULES={"auth":"my_app.migrations_auth",}The key ("auth") ofMIGRATION_MODULESis the name (app_label) of the library/app,
and the value is package/folder to store the migration files for this library/app.Note: this value will supercede/shield the original migrations folder in the library/app
(if it already uses django migrations), i.e.,django.contrib.auth.migrations.Next, runmanage.py makemigrations authandmanage.py migrate authto migrate the existing model as if for the first time (no matter whether the model used migrations before).
A new migration file0001_initial.pyshould be generated in the specified folder.
If the database table is already created for the model, no actual migrations will be applied.Add aSortedOneToManyFieldnameditemsto theUsermodel using the helper function:inject_extra_field_to_model(User,'items',SortedOneToManyField(Item,related_name='owner'))Runmanage.py makemigrations authandmanage.py migrate authagain to create the
intermediary table (auth_user_itemsby default).That’s it! Nowuser.itemsanditem.ownerare available as if you defined theitemsfield in theUsermodel source code.TestingSetup database:python manage.py makemigrations auth tests app2
python manage.py migrateRun tests:python manage.py test teststest_projectcontains the django projectsettings.pytestsfolder contains all the testcasesTested with django 1.8, 1.9 and Python 2.7, 3.3, 3.4, 3.5