├── django_sorting_field ├── __init__.py ├── templates │ └── sorting │ │ └── widgets │ │ └── sorting_widget.html ├── static │ └── sorting │ │ ├── css │ │ └── sorting_widget.css │ │ └── js │ │ ├── sorting_widget.js │ │ └── html.sortable.min.js ├── widgets.py ├── utils.py └── fields.py ├── requirements-test.txt ├── readme-media └── example.gif ├── MANIFEST.in ├── setup.py ├── .travis.yml ├── .gitignore ├── django_sorting_field_tests └── test_utils.py ├── setup.cfg ├── LICENSE └── README.rst /django_sorting_field/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | pytest 3 | pytest-cov 4 | -------------------------------------------------------------------------------- /readme-media/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andersinno/django-sorting-field/HEAD/readme-media/example.gif -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | 4 | recursive-include django_sorting_field *.css *.html *.js 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | if __name__ == '__main__': 4 | setuptools.setup( 5 | setup_requires=['setuptools>=34.0', 'setuptools-gitver'], 6 | gitver=True) 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | cache: pip 4 | python: 5 | - "3.4" 6 | - "2.7" 7 | install: 8 | - pip install -U pip 9 | - pip install -r requirements-test.txt 10 | script: 11 | - py.test -ra -vvv --cov 12 | after_success: 13 | - bash <(curl -s https://codecov.io/bash) 14 | -------------------------------------------------------------------------------- /django_sorting_field/templates/sorting/widgets/sorting_widget.html: -------------------------------------------------------------------------------- 1 |
2 | {% for item in items %} 3 |
{{ item.label }}
4 | {% endfor %} 5 |
6 | 7 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *,cover 2 | *.egg 3 | *.egg-info/ 4 | *.log 5 | *.manifest 6 | *.pot 7 | *.py[cod] 8 | *.so 9 | *.spec 10 | .cache 11 | .coverage 12 | .coverage.* 13 | .eggs/ 14 | .idea 15 | .installed.cfg 16 | .Python 17 | .tox/ 18 | __pycache__/ 19 | build/ 20 | coverage.xml 21 | develop-eggs/ 22 | dist/ 23 | docs/_build/ 24 | downloads/ 25 | eggs/ 26 | env/ 27 | htmlcov/ 28 | lib/ 29 | lib64/ 30 | nosetests.xml 31 | parts/ 32 | pip-delete-this-directory.txt 33 | pip-log.txt 34 | sdist/ 35 | target/ 36 | var/ 37 | venv/ 38 | -------------------------------------------------------------------------------- /django_sorting_field_tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from django_sorting_field.utils import sort_by_order 2 | 3 | 4 | class DummyItem(object): 5 | 6 | def __init__(self, item_id): 7 | self.id = item_id 8 | 9 | 10 | def test_sort_by_order_none(): 11 | items = [ 12 | DummyItem(0), 13 | DummyItem(1), 14 | DummyItem(2), 15 | ] 16 | sorted_items = sort_by_order(items, None) 17 | assert sorted_items[0].id == 0 18 | assert sorted_items[1].id == 1 19 | assert sorted_items[2].id == 2 20 | -------------------------------------------------------------------------------- /django_sorting_field/static/sorting/css/sorting_widget.css: -------------------------------------------------------------------------------- 1 | .sortable-widget-list { 2 | } 3 | 4 | .sortable-widget-list .sortable-widget-placeholder, 5 | .sortable-widget-list .sortable-widget-item { 6 | padding: 10px; 7 | margin-bottom: 5px; 8 | margin-top: 5px; 9 | color: rgb(255, 255, 255); 10 | background-color: #0bf; 11 | border: 1px solid #0bf; 12 | border-radius: 3px; 13 | cursor: pointer; 14 | } 15 | 16 | .sortable-widget-list .sortable-widget-placeholder { 17 | border: 1px dashed #0bf; 18 | background-color: inherit; 19 | } 20 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = django-sorting-field 3 | version = 1.0.2 4 | description = Django Sorting Field 5 | long_description = file: README.rst 6 | license = MIT 7 | license_file = LICENSE 8 | keywords = sorting, field, django 9 | 10 | [options] 11 | include_package_data = True 12 | packages = find: 13 | install_requires = 14 | django 15 | six 16 | 17 | [bdist_wheel] 18 | universal = 1 19 | 20 | [flake8] 21 | exclude = .tox,build,dist,migrations,doc/* 22 | max-complecity = 10 23 | 24 | [isort] 25 | not_skip=__init__.py 26 | skip=.tox,build,migrations 27 | known_first_party=django_sorting_field 28 | known_third_party=django,pytest,six 29 | multi_line_output=4 30 | -------------------------------------------------------------------------------- /django_sorting_field/static/sorting/js/sorting_widget.js: -------------------------------------------------------------------------------- 1 | const CreateSortableWidget = function(sortable_id) { 2 | var sortable_list_id = "#" + sortable_id + "_list"; 3 | 4 | var refreshInputValue = function() { 5 | result = []; 6 | $(sortable_list_id).children(".sortable-widget-item").each(function(index, element) { 7 | result.push($(element).data("id")); 8 | }); 9 | $("input#" + sortable_id).val(JSON.stringify(result)); 10 | } 11 | 12 | sortable(sortable_list_id, { 13 | placeholder: '
 
' 14 | })[0].addEventListener("sortstop", function() { 15 | refreshInputValue(); 16 | }); 17 | 18 | refreshInputValue(); 19 | }; 20 | -------------------------------------------------------------------------------- /django_sorting_field/widgets.py: -------------------------------------------------------------------------------- 1 | from django.forms.widgets import Widget 2 | from django.template.loader import render_to_string 3 | from django.utils.safestring import mark_safe 4 | 5 | 6 | class SortingWidget(Widget): 7 | template_name = 'sorting/widgets/sorting_widget.html' 8 | 9 | class Media: 10 | css = { 11 | "all": ("sorting/css/sorting_widget.css",) 12 | } 13 | js = ( 14 | "sorting/js/html.sortable.min.js", 15 | "sorting/js/sorting_widget.js", 16 | ) 17 | 18 | def render(self, name, value, attrs=None, renderer=None): 19 | context = attrs 20 | context.update({ 21 | "items": value, 22 | "name": name, 23 | }) 24 | html = render_to_string(self.template_name, context) 25 | return mark_safe(html) 26 | -------------------------------------------------------------------------------- /django_sorting_field/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import six 4 | 5 | 6 | def clean_order_json(value): 7 | value = "[]" if value is None else value 8 | 9 | if not isinstance(value, six.string_types): 10 | return value 11 | 12 | try: 13 | return json.loads(value) 14 | except ValueError: 15 | return [] 16 | 17 | 18 | def iterate_in_order(items, order): 19 | order = clean_order_json(order) 20 | items_by_id = {item.id: item for item in items} 21 | 22 | # Return items that are ordered first 23 | for entry in order: 24 | if entry not in items_by_id: 25 | continue 26 | yield items_by_id.pop(entry) 27 | 28 | # Return the rest 29 | for identifier, item in items_by_id.items(): 30 | yield item 31 | 32 | 33 | def sort_by_order(items, order): 34 | return [item for item in iterate_in_order(items, order)] 35 | -------------------------------------------------------------------------------- /django_sorting_field/fields.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django import forms 4 | from django.utils.encoding import force_text 5 | 6 | from .utils import clean_order_json, sort_by_order 7 | from .widgets import SortingWidget 8 | 9 | 10 | class SortedItem(object): 11 | 12 | def __init__(self, identifier, label): 13 | self.id = identifier 14 | self.label = label 15 | 16 | 17 | class SortingFormField(forms.CharField): 18 | 19 | def __init__(self, *args, **kwargs): 20 | kwargs.update({ 21 | "widget": SortingWidget(), 22 | "required": False, 23 | }) 24 | self.items = [] 25 | super(SortingFormField, self).__init__(*args, **kwargs) 26 | 27 | def populate(self, items): 28 | self.items = [SortedItem(item.pk, force_text(item)) for item in items] 29 | 30 | def prepare_value(self, value): 31 | value = clean_order_json(value) 32 | return sort_by_order(self.items, value) 33 | 34 | def to_python(self, value): 35 | value = clean_order_json(value) 36 | return json.dumps(value) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Anders Innovations 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Sorting Field 2 | ==================== 3 | .. image:: https://travis-ci.org/andersinno/django-sorting-field.svg?branch=master 4 | :target: https://travis-ci.org/andersinno/django-sorting-field 5 | .. image:: https://codecov.io/gh/andersinno/django-sorting-field/branch/master/graph/badge.svg 6 | :target: https://codecov.io/gh/andersinno/django-sorting-field 7 | 8 | * This package implements a Django form field + widget for drag & drog sorting of items 9 | * Sorting any item with a field called ``id`` is supported 10 | * The drag and drop feature has been implemented with `html5sortable `_. 11 | 12 | Known limitations 13 | ----------------- 14 | 15 | * Refreshing the items on the widget not (yet?) supported out of the box 16 | * No tests 17 | 18 | Example of the widget 19 | --------------------- 20 | 21 | .. image:: readme-media/example.gif 22 | 23 | Usage 24 | ----- 25 | 26 | The sort order field should be implemented on the model containing the sorted objects. 27 | This allows ordering of different instances of the same item set differently. 28 | 29 | Let's say you have image CarouselPlugin, Carousel, and Picture models, and you wish to be able to 30 | sort the same Carousel instance differently on each CarouselPlugin. 31 | 32 | You also have a CMSPlugin object for the carousel. 33 | 34 | .. code-block:: python 35 | 36 | class Carousel(models.Model): 37 | pass 38 | 39 | 40 | class Picture(models.Model): 41 | carousel = models.ForeignKey(Carousel, related_name="pictures") 42 | image = SomeImageField() 43 | name = models.CharField() 44 | 45 | 46 | class CarouselPlugin(CMSPlugin): 47 | carousel = models.ForeignKey(Carousel, related_name="x") 48 | 49 | 50 | class CMSCarouselPlugin(CMSPluginBase): 51 | model = CarouselPlugin 52 | 53 | def render(self, context, instance, placeholder): 54 | context.update({ 55 | "pictures": self.instance.carousel.pictures.all(), 56 | }) 57 | return context 58 | 59 | 60 | Achieving the wanted behavior can be done in the following steps: 61 | 62 | Add a (nullable) TextField to the model containing the order information 63 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 64 | 65 | .. code-block:: python 66 | 67 | class CarouselPlugin(CMSPlugin): 68 | carousel = models.ForeignKey(Carousel, related_name="x") 69 | carousel_order = models.TextField(null=True) 70 | 71 | 72 | Add the SortingFormField to the CMS Plugin and populate it 73 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 74 | 75 | .. code-block:: python 76 | 77 | from django_sorting_field.fields import SortingFormField 78 | 79 | class CarouselPluginForm(forms.ModelForm): 80 | carousel_order = SortingFormField() 81 | 82 | def __init__(self, *args, **kwargs): 83 | super(CarouselPluginForm, self).__init__(*args, **kwargs) 84 | if self.instance.pk: 85 | self.fields["carousel_order"].populate( 86 | items=self.instance.carousel.pictures.all(), 87 | ) 88 | 89 | class CMSCarouselPlugin(CMSPluginBase): 90 | model = CarouselPlugin 91 | form = CarouselPluginForm 92 | 93 | def render(self, context, instance, placeholder): 94 | context.update({ 95 | "pictures": self.instance.carousel.pictures.all(), 96 | }) 97 | return context 98 | 99 | Finally, sort the items passed to the context data 100 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 101 | 102 | .. code-block:: python 103 | 104 | from django_sorting_field.utils import sort_by_order 105 | 106 | class CMSCarouselPlugin(CMSPluginBase): 107 | model = CarouselPlugin 108 | form = CarouselPluginForm 109 | 110 | def render(self, context, instance, placeholder): 111 | context.update({ 112 | "pictures": sort_by_order( 113 | self.instance.carousel.pictures.all(), 114 | self.instance.carousel_order 115 | ), 116 | }) 117 | return context 118 | -------------------------------------------------------------------------------- /django_sorting_field/static/sorting/js/html.sortable.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?module.exports=t():e.sortable=t()}(this,function(){"use strict";function e(e,t,n){var r=null;return 0===t?e:function(){var a=n||this,o=arguments;clearTimeout(r),r=setTimeout(function(){e.apply(a,o)},t)}}var t,n,r=[],a=[],o=function(e,t,n){return void 0===n?e&&e.h5s&&e.h5s.data&&e.h5s.data[t]:(e.h5s=e.h5s||{},e.h5s.data=e.h5s.data||{},e.h5s.data[t]=n,void 0)},i=function(e){e.h5s&&delete e.h5s.data},s=function(e,t){return(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector).call(e,t)},l=function(e,t){if(!t)return Array.prototype.slice.call(e);for(var n=[],r=0;rn){var d=o-n,f=p(e).top;if(is&&a>f+o-d)return}void 0===t.oldDisplay&&(t.oldDisplay=t.style.display),"none"!==t.style.display&&(t.style.display="none"),i=v||(e.preventDefault(),e.stopPropagation(),e.dataTransfer.dropEffect="move",C(this,e.pageY)))};d(y.concat(i),"dragover",S),d(y.concat(i),"dragenter",S)}),i};return q.destroy=function(e){C(e)},q.enable=function(e){A(e)},q.disable=function(e){S(e)},q}); 2 | //# sourceMappingURL=html.sortable.min.js.map 3 | --------------------------------------------------------------------------------