Django Models has limited option when it comes to ManyToMany fields. Say you have a Image model with a records: image1, image2 .. upto image7. And you want to create a Gallery model as a list of images that has a specific order image4, image7, image2. Then we need to have image4, 7 and 2 inserted into Image model in the same order. Else the multi selection widget will not allow us to select in random order and hence the gallery will always have objects in the same order as has been inserted into Image model.
To solve this issue Django provides a through argument that allows to create another model to govern the many-to-many relationship between your Gallery and Image model. Say we call Order Model and then our model class will look like this:
class Gallery(models.Model): name = models.CharField(max_length=200, null=False, blank=False, unique=True) image = models.ForeignKey('Image',through='Order') class Image(models.Model): name = models.CharField(max_length=200, null=False, blank=False, unique=True) image = models.ImageField(upload_to='folder-path/static/img') class Order(models.Model): image = models.ForeignKey('Image') gallery = models.ForeignKey('Gallery')
The problem with this method is, it is quite crude way to implement such a thing as everytime you create a gallery with 10 Image records you have to enter 10 records to Order model to say in what order they should be pushed into Gallery. A much cleaner way of doing this is by using a external javascript which will fake a list of Image dropdowns in a ordered pattern and allow the user to enter the images in the order needed and then saving it to json object and sending it to backend to save as a field. Following is a simple implementation of this:
Django models would look like this:
class Gallery(models.Model): name = models.CharField(max_length=200, null=False, blank=False, unique=True) size = models.IntegerField(null=False, blank=False, default=1, help_text='Enter the number of images in the gallery. Default Min=1') #this field will be duplicated to create a fake ordering for user image = models.ForeignKey('Image') #this will be a hidden field to maintain our ordering image_order = JSONField() class Image(models.Model): name = models.CharField(max_length=200, null=False, blank=False, unique=True) image = models.ImageField(upload_to='folder-path/static/img') url = models.CharField(max_length=3000, null=True, blank=True, help_text='Path that will render this image')
The admin.py for this model would define the js file and save method as follows:
class GalleryAdmin(admin.ModelAdmin): class Media: js = ('admin/js/admin_custom_gallery.js',) @staticmethod def save_model(request, obj, form, change): if type(obj.image_order) is not str: obj.image_order = json.dumps(obj.image_order) obj.save() class ImageAdmin(admin.ModelAdmin): @staticmethod def save_model(request, obj, form, change): obj.url = "img/" + str(obj.image) obj.save()
And the javascript file would look like this:
var $ = django.jQuery; $(document).ready(function(){ //Set the image order json field in admin to display none $('.image_order').css('display','none'); var size = $('#id_size').val(); /*Count maintains the count of dropdowns and index is the position. The json object is the input to the field that maintains the list of images and their order when saving gallery object */ var count = 1; var index = 0; var jsonobject = {} /* We use the on function to check if there is a change in the size of gallery item. If the user changes it to a new value greater than current value, we add dropdowns else we delete them. Also we account for the case where the user deletes the size to blank before entering a new value and we ignore any changes for this case */ $('#id_size').on('input', function() { if ($('#id_size').val()!=''){ size = $('#id_size').val(); //when element deleted remove dropdown and delete from jsonobject list if ($('#id_size').val() < count ) { while (count > $('#id_size').val()){ if ($('#id_size').val() == 0){ alert("Gallery size cannot be zero."); $('#id_size').val(Object.keys(jsonobject).length); break; } $('#id_image_'+index).remove(); delete jsonobject[index]; $('#id_image_order').text(JSON.stringify(jsonobject)); index--; count--; } } //when element added add a new dropdown with blank value else{ while (count < $('#id_size').val()){ if (index == 0){ $('#id_image').clone().attr('id', 'id_image_'+count).insertAfter('#id_image'); $("#id_image_"+count).val(""); } else{ $('#id_image_'+index).clone().attr('id', 'id_image_'+count).insertAfter('#id_image_'+index); $("#id_image_"+count).val(""); } index++; count++; } } //when blank dropdown value changes update jsonobject list $('[id^="id_image"]').change(function() { var id = $(this).attr('id').substring($(this).attr('id').lastIndexOf("_") + 1, $(this).attr('id').length); if (id=="image"){ jsonobject[0] = $(this).val(); } else { jsonobject[id] = $(this).val(); } $('#id_image_order').text(JSON.stringify(jsonobject)); }); } }); //if gallery object already exist we loop through images and create pre-populated dropdowns if ($('#id_image_order').length > 0) { var imageorder = JSON.parse($('#id_image_order').val(). replace(/\\/g, '').replace(/"{/g, '{').replace(/}"/g, '}')); //first create dropdowns while (count < $('#id_size').val()){ if (index == 0){ $('#id_image').clone().attr('id', 'id_image_'+count).insertAfter('#id_image'); } else{ $('#id_image_'+index).clone().attr('id', 'id_image_'+count).insertAfter('#id_image_'+index); } index++; count++; } //populate dropdowns with selected values $.each(imageorder, function(k, v) { if (k==0){ $('#id_image option[value='+v+']').attr("selected","selected"); } else{ $('#id_image_'+k+' option[value='+v+']').attr("selected","selected"); } }); jsonobject = imageorder; } //when existing dropdown value changes update jsonobject list $('[id^="id_image"]').change(function() { var id = $(this).attr('id').substring($(this).attr('id').lastIndexOf("_") + 1, $(this).attr('id').length); if (id=="image"){ jsonobject[0] = $(this).val(); } else { jsonobject[id] = $(this).val(); } $('#id_image_order').text(JSON.stringify(jsonobject)); }); });
Last but not the least when retrieving values in views, this can be done using:
for gal in Gallery.objects.all(): image_order = json.loads(gal.image_order) imagelist = [] for key in range(0, len(image_order)): imagelist.append(Image.objects.get(id=int(image_order[str(key)]))) gallery[gal.name] = imagelist
This gives a simple example of how to use javascript/jquery to implement admin functionality in Django.