├── .flake8 ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── django_admin_hstore_widget ├── __init__.py ├── forms.py ├── locale │ └── ru │ │ └── LC_MESSAGES │ │ └── django.po ├── static │ └── admin │ │ └── js │ │ └── django_admin_hstore_widget │ │ ├── django_admin_hstore_widget.js │ │ └── underscore-min.js ├── templates │ └── django_admin_hstore_widget.html └── widgets.py ├── requirements ├── maintainer.txt ├── test-ci.txt └── test.txt ├── results.png ├── runtests.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── models.py ├── settings.py ├── test_hstore_field.py └── urls.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = __pycache__ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | #Mac os X Files 104 | .DS_Store 105 | 106 | #JetBrain files 107 | .idea/ 108 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: pip 3 | dist: xenial 4 | services: 5 | - postgresql 6 | 7 | install: 8 | - pip install coverage tox tox-venv tox-travis flake8 psycopg2 9 | 10 | before_script: 11 | - psql template1 -c 'create extension hstore;' 12 | 13 | script: 14 | - coverage erase 15 | - tox 16 | - flake8 django_admin_hstore_widget/ 17 | 18 | after_success: 19 | - coverage combine --append 20 | - coverage report -m 21 | - pip install codecov 22 | - codecov 23 | 24 | matrix: 25 | fast_finish: true 26 | include: 27 | # PYTHON 3.9 28 | - { python: "3.9", env: DJANGO=3.0 } 29 | - { python: "3.9", env: DJANGO=4.0 } 30 | # PYTHON 3.8 31 | - { python: "3.8", env: DJANGO=3.0 } 32 | - { python: "3.8", env: DJANGO=4.0 } 33 | # PYTHON 3.7 34 | - { python: "3.7", env: DJANGO=3.0 } 35 | - { python: "3.7", env: DJANGO=2.2 } 36 | - { python: "3.7", env: DJANGO=2.1 } 37 | - { python: "3.7", env: DJANGO=2.0 } 38 | - { python: "3.7", env: DJANGO=1.11 } 39 | # PYTHON 3.6 40 | - { python: "3.6", env: DJANGO=3.0 } 41 | - { python: "3.6", env: DJANGO=2.2 } 42 | - { python: "3.6", env: DJANGO=2.1 } 43 | - { python: "3.6", env: DJANGO=2.0 } 44 | - { python: "3.6", env: DJANGO=1.11 } 45 | # PYTHON 3.5 46 | - { python: "3.5", env: DJANGO=2.2 } 47 | - { python: "3.5", env: DJANGO=2.1 } 48 | - { python: "3.5", env: DJANGO=2.0 } 49 | - { python: "3.5", env: DJANGO=1.11 } 50 | # PYTHON 3.4 51 | - { python: "3.4", env: DJANGO=2.0 } 52 | - { python: "3.4", env: DJANGO=1.11 } 53 | # PYTHON 2.7 54 | - { python: "2.7", env: DJANGO=1.11 } 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Poka 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean, build 2 | 3 | deps-maintainer: 4 | pip install -r ./requirements/maintainer.txt 5 | 6 | check: 7 | flake8 django_admin_hstore_widget/ 8 | 9 | clean: 10 | rm -rfv build 11 | rm -rfv dist 12 | rm -rfv django_admin_hstore_widget.egg-info 13 | 14 | build: 15 | python setup.py sdist 16 | 17 | upload: clean build 18 | twine upload dist/* 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-admin-hstore-widget (Abandoned) 2 | 3 | ## This project is officially abandoned and won't receive any new updates. This repository will be archived as of January 2025 4 | There's a hard fork of this project that is actively maintained and has more features. You can find more information [here](https://github.com/PokaInc/django-admin-hstore-widget/issues/27) 5 | 6 | ### New fork 7 | [GitHub](https://github.com/PokaInc/django-admin-hstore-widget/issues/27) 8 | 9 | [pypi](https://pypi.org/project/django-hstore-widget/) 10 | 11 | 12 | --- 13 | 14 | FormField that properly render HStoreField Data in django Admin based on [djangoauts package](https://github.com/djangonauts/django-hstore) 15 | 16 | [![Build Status](https://travis-ci.org/PokaInc/django-admin-hstore-widget.svg?branch=master)](https://travis-ci.org/PokaInc/django-admin-hstore-widget) 17 | 18 | 19 | ## Requirements 20 | * Python 2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9 21 | * Django 1.11, 2.0, 2.1, 2.2, 3.0, 4.0 22 | 23 | 24 | Using pip: 25 | ```bash 26 | pip install django-admin-hstore-widget 27 | ``` 28 | 29 | ## Installation 30 | 31 | Add django_admin_hstore_widget to your INSTALLED_APPS ( in base.py ) 32 | 33 | ## Usage 34 | 35 | ```python 36 | # yourmodel/admin.py 37 | from django.contrib import admin 38 | from django import forms 39 | 40 | from django_admin_hstore_widget.forms import HStoreFormField 41 | from models import Yourmodel 42 | 43 | class MyModelAdminForm(forms.ModelForm): 44 | my_hstore_field = HStoreFormField() 45 | 46 | class Meta: 47 | model = Yourmodel 48 | exclude = () 49 | 50 | @admin.register(Yourmodel) 51 | class YourmodelAdmin(admin.ModelAdmin): 52 | form = MyModelAdminForm 53 | 54 | ``` 55 | 56 | ## Result 57 | 58 | ![Rendered result](results.png) 59 | -------------------------------------------------------------------------------- /django_admin_hstore_widget/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.2.1' 2 | -------------------------------------------------------------------------------- /django_admin_hstore_widget/forms.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.contrib.postgres.forms import HStoreField 3 | 4 | from .widgets import HStoreFormWidget 5 | 6 | 7 | class HStoreFormField(HStoreField): 8 | widget = HStoreFormWidget 9 | 10 | def clean(self, value): 11 | if not value: 12 | value = '{}' 13 | return json.loads(value) 14 | -------------------------------------------------------------------------------- /django_admin_hstore_widget/locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2018-08-22 16:49+0800\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 19 | "%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" 20 | "%100>=11 && n%100<=14)? 2 : 3);\n" 21 | #: .\templates\django_admin_hstore_widget.html:5 22 | msgid "key" 23 | msgstr "ключ" 24 | 25 | #: .\templates\django_admin_hstore_widget.html:10 26 | msgid "value" 27 | msgstr "значение" 28 | 29 | #: .\templates\django_admin_hstore_widget.html:12 30 | msgid "remove row" 31 | msgstr "удалить строку" 32 | 33 | #: .\templates\django_admin_hstore_widget.html:46 34 | msgid "Raw textarea" 35 | msgstr "Текстовое представление" 36 | 37 | #: .\templates\django_admin_hstore_widget.html:52 38 | msgid "Add another row" 39 | msgstr "Добавить другую строку" 40 | 41 | #: .\templates\django_admin_hstore_widget.html:53 42 | msgid "Add Another" 43 | msgstr "Добавить другую" 44 | 45 | #: .\templates\django_admin_hstore_widget.html:54 46 | msgid "Add row" 47 | msgstr "Добавить строку" 48 | 49 | #: .\templates\django_admin_hstore_widget.html:57 50 | #: .\templates\django_admin_hstore_widget.html:58 51 | #: .\templates\django_admin_hstore_widget.html:59 52 | msgid "toggle textarea" 53 | msgstr "переключить представление" 54 | -------------------------------------------------------------------------------- /django_admin_hstore_widget/static/admin/js/django_admin_hstore_widget/django_admin_hstore_widget.js: -------------------------------------------------------------------------------- 1 | var initDjangoHStoreWidget = function(hstore_field_name, inline_prefix) { 2 | // ignore inline templates 3 | // if hstore_field_name contains "__prefix__" 4 | if(hstore_field_name.indexOf('__prefix__') > -1){ 5 | return; 6 | } 7 | 8 | var $ = django.jQuery; 9 | 10 | // processing inlines 11 | if(hstore_field_name.indexOf('inline') > -1){ 12 | var inlineClass = $('#id_'+hstore_field_name).parents('.inline-related, .grp-group').attr('class'); 13 | // if using TabularInlines stop here 14 | // TabularInlines not supported 15 | if (inlineClass.indexOf('tabular') > -1) { 16 | return; 17 | } 18 | } 19 | 20 | // reusable function that retrieves a template even if ID is not correct 21 | // (written to support inlines) 22 | var retrieveTemplate = function(template_name, field_name){ 23 | var specific_template = $('#'+template_name+'-'+field_name); 24 | // if found specific template return that 25 | if(specific_template.length){ 26 | return specific_template.html(); 27 | } 28 | else{ 29 | // get fallback template 30 | var html = $('.'+template_name+'-inline').html(); 31 | // replace all occurrences of __prefix__ with field_name 32 | // and return 33 | html = html.replace(/__prefix__/g, inline_prefix); 34 | return html; 35 | } 36 | } 37 | 38 | // reusable function that compiles the UI 39 | var compileUI = function(params){ 40 | var hstore_field_id = 'id_'+hstore_field_name, 41 | original_textarea = $('#'+hstore_field_id), 42 | original_value = original_textarea.val(), 43 | original_container = original_textarea.parents('.form-row, .grp-row').eq(0), 44 | errorHtml = original_container.find('.errorlist').html(), 45 | json_data = {}; 46 | 47 | if(original_value !== ''){ 48 | // manage case in which textarea is blank 49 | try{ 50 | json_data = JSON.parse(original_value); 51 | } 52 | catch(e){ 53 | alert('invalid JSON:\n'+e); 54 | return false; 55 | } 56 | } 57 | 58 | var hstore_field_data = { 59 | "id": hstore_field_id, 60 | "label": original_container.find('label').text(), 61 | "name": hstore_field_name, 62 | "value": original_textarea.val(), 63 | "help": original_container.find('.grp-help, .help').text(), 64 | "errors": errorHtml, 65 | "data": json_data 66 | }, 67 | // compile template 68 | ui_html = retrieveTemplate('hstore-ui-template', hstore_field_name), 69 | compiled_ui_html = _.template(ui_html, hstore_field_data); 70 | 71 | // this is just to DRY up a bit 72 | if(params && params.replace_original === true){ 73 | // remove original textarea to avoid having two textareas with same ID 74 | original_textarea.remove(); 75 | // inject compiled template and hide original 76 | original_container.after(compiled_ui_html).hide(); 77 | } 78 | 79 | return compiled_ui_html; 80 | }; 81 | 82 | 83 | 84 | // generate UI 85 | compileUI({ replace_original: true }); 86 | 87 | // cache other objects that we'll reuse 88 | var row_html = retrieveTemplate('hstore-row-template', hstore_field_name), 89 | empty_row = _.template(row_html, { 'key': '', 'value': '' }), 90 | $hstore = $('#id_'+hstore_field_name).parents('.hstore'); 91 | 92 | // reusable function that updates the textarea value 93 | var updateTextarea = function(container) { 94 | // init empty json object 95 | var new_value = {}, 96 | raw_textarea = container.find('textarea'), 97 | rows = container.find('.form-row, .grp-row'); 98 | 99 | // loop over each object and populate json 100 | rows.each(function() { 101 | var inputs = $(this).find('input'), 102 | key = inputs.eq(0).val(), 103 | value = inputs.eq(1).val(); 104 | new_value[key] = value; 105 | }); 106 | 107 | // update textarea value 108 | $(raw_textarea).val(JSON.stringify(new_value, null, 4)); 109 | }; 110 | 111 | // remove row link 112 | $hstore.delegate('a.remove-row', 'click', function(e) { 113 | e.preventDefault(); 114 | // cache container jquery object before $(this) gets removed 115 | $(this).parents('.form-row, .grp-row').eq(0).remove(); 116 | updateTextarea($hstore); 117 | }); 118 | 119 | // add row link 120 | $hstore.delegate('a.hs-add-row, .hs-add-row a', 'click', function(e) { 121 | e.preventDefault(); 122 | $hstore.find('.hstore-rows').append(empty_row); 123 | }); 124 | 125 | // toggle textarea link 126 | $hstore.delegate('.hstore-toggle-txtarea', 'click', function(e) { 127 | e.preventDefault(); 128 | 129 | var raw_textarea = $hstore.find('.hstore-textarea'), 130 | hstore_rows = $hstore.find('.hstore-rows'), 131 | add_row = $hstore.find('.hs-add-row'); 132 | 133 | if(raw_textarea.is(':visible')) { 134 | 135 | var compiled_ui = compileUI(); 136 | 137 | // in case of JSON error 138 | if(compiled_ui === false){ 139 | return; 140 | } 141 | 142 | // jquery < 1.8 143 | try{ 144 | var $ui = $(compiled_ui); 145 | } 146 | // jquery >= 1.8 147 | catch(e){ 148 | var $ui = $($.parseHTML(compiled_ui)); 149 | } 150 | 151 | // update rows with only relevant content 152 | hstore_rows.html($ui.find('.hstore-rows').html()); 153 | 154 | raw_textarea.hide(); 155 | hstore_rows.show(); 156 | add_row.show(); 157 | } 158 | else{ 159 | raw_textarea.show(); 160 | hstore_rows.hide(); 161 | add_row.hide(); 162 | } 163 | }); 164 | 165 | // update textarea whenever a field changes 166 | $hstore.delegate('input[type=text]', 'input propertychange', function() { 167 | updateTextarea($hstore); 168 | }); 169 | }; 170 | 171 | django.jQuery(window).on('load', function() { 172 | // support inlines 173 | // bind only once 174 | if(django.hstoreWidgetBoundInlines === undefined){ 175 | var $ = django.jQuery; 176 | $('.grp-group .grp-add-handler, .inline-group .hs-add-row a, .inline-group .add-row').click(function(e){ 177 | var hstore_original_textareas = $(this).parents('.grp-group, .inline-group').eq(0).find('.hstore-original-textarea'); 178 | // if module contains .hstore-original-textarea 179 | if(hstore_original_textareas.length > 0){ 180 | // loop over each inline 181 | $(this).parents('.grp-group, .inline-group').find('.grp-items div.grp-dynamic-form, .inline-related').each(function(e, i){ 182 | var prefix = i; 183 | // loop each textarea 184 | $(this).find('.hstore-original-textarea').each(function(e, i){ 185 | // cache field name 186 | var field_name = $(this).attr('name'); 187 | // ignore templates 188 | // if name attribute contains __prefix__ 189 | if(field_name.indexOf('prefix') > -1){ 190 | // skip to next 191 | return; 192 | } 193 | initDjangoHStoreWidget(field_name, prefix); 194 | }); 195 | }); 196 | } 197 | }); 198 | django.hstoreWidgetBoundInlines = true; 199 | } 200 | }); 201 | -------------------------------------------------------------------------------- /django_admin_hstore_widget/static/admin/js/django_admin_hstore_widget/underscore-min.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.5.2 2 | // http://underscorejs.org 3 | // (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | (function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,h=e.reduce,v=e.reduceRight,g=e.filter,d=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,w=Object.keys,_=i.bind,j=function(n){return n instanceof j?n:this instanceof j?(this._wrapped=n,void 0):new j(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=j),exports._=j):n._=j,j.VERSION="1.5.2";var A=j.each=j.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a=j.keys(n),u=0,i=a.length;i>u;u++)if(t.call(e,n[a[u]],a[u],n)===r)return};j.map=j.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e.push(t.call(r,n,u,i))}),e)};var E="Reduce of empty array with no initial value";j.reduce=j.foldl=j.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduce===h)return e&&(t=j.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(E);return r},j.reduceRight=j.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduceRight===v)return e&&(t=j.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=j.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(E);return r},j.find=j.detect=function(n,t,r){var e;return O(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},j.filter=j.select=function(n,t,r){var e=[];return null==n?e:g&&n.filter===g?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&e.push(n)}),e)},j.reject=function(n,t,r){return j.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},j.every=j.all=function(n,t,e){t||(t=j.identity);var u=!0;return null==n?u:d&&n.every===d?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var O=j.some=j.any=function(n,t,e){t||(t=j.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};j.contains=j.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:O(n,function(n){return n===t})},j.invoke=function(n,t){var r=o.call(arguments,2),e=j.isFunction(t);return j.map(n,function(n){return(e?t:n[t]).apply(n,r)})},j.pluck=function(n,t){return j.map(n,function(n){return n[t]})},j.where=function(n,t,r){return j.isEmpty(t)?r?void 0:[]:j[r?"find":"filter"](n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},j.findWhere=function(n,t){return j.where(n,t,!0)},j.max=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.max.apply(Math,n);if(!t&&j.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>e.computed&&(e={value:n,computed:a})}),e.value},j.min=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.min.apply(Math,n);if(!t&&j.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;ae||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={},i=null==r?j.identity:k(r);return A(t,function(r,a){var o=i.call(e,r,a,t);n(u,o,r)}),u}};j.groupBy=F(function(n,t,r){(j.has(n,t)?n[t]:n[t]=[]).push(r)}),j.indexBy=F(function(n,t,r){n[t]=r}),j.countBy=F(function(n,t){j.has(n,t)?n[t]++:n[t]=1}),j.sortedIndex=function(n,t,r,e){r=null==r?j.identity:k(r);for(var u=r.call(e,t),i=0,a=n.length;a>i;){var o=i+a>>>1;r.call(e,n[o])=0})})},j.difference=function(n){var t=c.apply(e,o.call(arguments,1));return j.filter(n,function(n){return!j.contains(t,n)})},j.zip=function(){for(var n=j.max(j.pluck(arguments,"length").concat(0)),t=new Array(n),r=0;n>r;r++)t[r]=j.pluck(arguments,""+r);return t},j.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},j.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=j.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},j.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},j.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=new Array(e);e>u;)i[u++]=n,n+=r;return i};var R=function(){};j.bind=function(n,t){var r,e;if(_&&n.bind===_)return _.apply(n,o.call(arguments,1));if(!j.isFunction(n))throw new TypeError;return r=o.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(o.call(arguments)));R.prototype=n.prototype;var u=new R;R.prototype=null;var i=n.apply(u,r.concat(o.call(arguments)));return Object(i)===i?i:u}},j.partial=function(n){var t=o.call(arguments,1);return function(){return n.apply(this,t.concat(o.call(arguments)))}},j.bindAll=function(n){var t=o.call(arguments,1);if(0===t.length)throw new Error("bindAll must be passed function names");return A(t,function(t){n[t]=j.bind(n[t],n)}),n},j.memoize=function(n,t){var r={};return t||(t=j.identity),function(){var e=t.apply(this,arguments);return j.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},j.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},j.defer=function(n){return j.delay.apply(j,[n,1].concat(o.call(arguments,1)))},j.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var c=function(){o=r.leading===!1?0:new Date,a=null,i=n.apply(e,u)};return function(){var l=new Date;o||r.leading!==!1||(o=l);var f=t-(l-o);return e=this,u=arguments,0>=f?(clearTimeout(a),a=null,o=l,i=n.apply(e,u)):a||r.trailing===!1||(a=setTimeout(c,f)),i}},j.debounce=function(n,t,r){var e,u,i,a,o;return function(){i=this,u=arguments,a=new Date;var c=function(){var l=new Date-a;t>l?e=setTimeout(c,t-l):(e=null,r||(o=n.apply(i,u)))},l=r&&!e;return e||(e=setTimeout(c,t)),l&&(o=n.apply(i,u)),o}},j.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},j.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},j.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},j.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},j.keys=w||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)j.has(n,r)&&t.push(r);return t},j.values=function(n){for(var t=j.keys(n),r=t.length,e=new Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},j.pairs=function(n){for(var t=j.keys(n),r=t.length,e=new Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},j.invert=function(n){for(var t={},r=j.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},j.functions=j.methods=function(n){var t=[];for(var r in n)j.isFunction(n[r])&&t.push(r);return t.sort()},j.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},j.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},j.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)j.contains(r,u)||(t[u]=n[u]);return t},j.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]===void 0&&(n[r]=t[r])}),n},j.clone=function(n){return j.isObject(n)?j.isArray(n)?n.slice():j.extend({},n):n},j.tap=function(n,t){return t(n),n};var S=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof j&&(n=n._wrapped),t instanceof j&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==String(t);case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;var a=n.constructor,o=t.constructor;if(a!==o&&!(j.isFunction(a)&&a instanceof a&&j.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c=0,f=!0;if("[object Array]"==u){if(c=n.length,f=c==t.length)for(;c--&&(f=S(n[c],t[c],r,e)););}else{for(var s in n)if(j.has(n,s)&&(c++,!(f=j.has(t,s)&&S(n[s],t[s],r,e))))break;if(f){for(s in t)if(j.has(t,s)&&!c--)break;f=!c}}return r.pop(),e.pop(),f};j.isEqual=function(n,t){return S(n,t,[],[])},j.isEmpty=function(n){if(null==n)return!0;if(j.isArray(n)||j.isString(n))return 0===n.length;for(var t in n)if(j.has(n,t))return!1;return!0},j.isElement=function(n){return!(!n||1!==n.nodeType)},j.isArray=x||function(n){return"[object Array]"==l.call(n)},j.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){j["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),j.isArguments(arguments)||(j.isArguments=function(n){return!(!n||!j.has(n,"callee"))}),"function"!=typeof/./&&(j.isFunction=function(n){return"function"==typeof n}),j.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},j.isNaN=function(n){return j.isNumber(n)&&n!=+n},j.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},j.isNull=function(n){return null===n},j.isUndefined=function(n){return n===void 0},j.has=function(n,t){return f.call(n,t)},j.noConflict=function(){return n._=t,this},j.identity=function(n){return n},j.times=function(n,t,r){for(var e=Array(Math.max(0,n)),u=0;n>u;u++)e[u]=t.call(r,u);return e},j.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))};var I={escape:{"&":"&","<":"<",">":">",'"':""","'":"'"}};I.unescape=j.invert(I.escape);var T={escape:new RegExp("["+j.keys(I.escape).join("")+"]","g"),unescape:new RegExp("("+j.keys(I.unescape).join("|")+")","g")};j.each(["escape","unescape"],function(n){j[n]=function(t){return null==t?"":(""+t).replace(T[n],function(t){return I[n][t]})}}),j.result=function(n,t){if(null==n)return void 0;var r=n[t];return j.isFunction(r)?r.call(n):r},j.mixin=function(n){A(j.functions(n),function(t){var r=j[t]=n[t];j.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),z.call(this,r.apply(j,n))}})};var N=0;j.uniqueId=function(n){var t=++N+"";return n?n+t:t},j.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var q=/(.)^/,B={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\t|\u2028|\u2029/g;j.template=function(n,t,r){var e;r=j.defaults({},r,j.templateSettings);var u=new RegExp([(r.escape||q).source,(r.interpolate||q).source,(r.evaluate||q).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(D,function(n){return"\\"+B[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=new Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,j);var c=function(n){return e.call(this,n,j)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},j.chain=function(n){return j(n).chain()};var z=function(n){return this._chain?j(n).chain():n};j.mixin(j),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];j.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],z.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];j.prototype[n]=function(){return z.call(this,t.apply(this._wrapped,arguments))}}),j.extend(j.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this); 6 | -------------------------------------------------------------------------------- /django_admin_hstore_widget/templates/django_admin_hstore_widget.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | 18 | 19 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /django_admin_hstore_widget/widgets.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.contrib.admin.templatetags.admin_static import static 3 | except ImportError: 4 | from django.templatetags.static import static 5 | from django.contrib.admin.widgets import AdminTextareaWidget 6 | from django.contrib.postgres.forms import forms 7 | from django.template.loader import get_template 8 | from django.utils.safestring import mark_safe 9 | 10 | 11 | class HStoreFormWidget(AdminTextareaWidget): 12 | 13 | @property 14 | def media(self): 15 | internal_js = [ 16 | "django_admin_hstore_widget/underscore-min.js", 17 | "django_admin_hstore_widget/django_admin_hstore_widget.js" 18 | ] 19 | 20 | js = [static("admin/js/%s" % path) for path in internal_js] 21 | 22 | return forms.Media(js=js) 23 | 24 | def render(self, name, value, attrs=None, renderer=None): 25 | if attrs is None: 26 | attrs = {} 27 | # it's called "original" because it will be replaced by a copy 28 | attrs['class'] = 'hstore-original-textarea' 29 | 30 | # get default HTML from AdminTextareaWidget 31 | html = super(HStoreFormWidget, self).render(name, value, attrs) 32 | 33 | # prepare template context 34 | template_context = { 35 | 'field_name': name, 36 | } 37 | 38 | # get template object 39 | template = get_template('django_admin_hstore_widget.html') 40 | # render additional html 41 | additional_html = template.render(template_context) 42 | 43 | # append additional HTML and mark as safe 44 | html = html + additional_html 45 | html = mark_safe(html) 46 | 47 | return html 48 | -------------------------------------------------------------------------------- /requirements/maintainer.txt: -------------------------------------------------------------------------------- 1 | twine 2 | bumpversion 3 | -------------------------------------------------------------------------------- /requirements/test-ci.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | freezegun 3 | psycopg2 -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | -r test-ci.txt 2 | django 3 | flake8 4 | django-admin-hstore-widget 5 | psycopg2 -------------------------------------------------------------------------------- /results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokaInc/django-admin-hstore-widget/d3438c376fb04daa664b5b31dd60937733ae375f/results.png -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | from django.core.management import execute_from_command_line 6 | 7 | 8 | def runtests(): 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") 10 | argv = sys.argv[:1] + ['test'] + sys.argv[1:] 11 | execute_from_command_line(argv) 12 | 13 | 14 | if __name__ == '__main__': 15 | runtests() 16 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | version = '1.2.1' 5 | 6 | 7 | def get_package_data(package): 8 | """ 9 | Return all files under the root package, that are not in a 10 | package themselves. 11 | """ 12 | walk = [(dirpath.replace(package + os.sep, '', 1), filenames) 13 | for dirpath, dirnames, filenames in os.walk(package) 14 | if not os.path.exists(os.path.join(dirpath, '__init__.py'))] 15 | 16 | filepaths = [] 17 | for base, filenames in walk: 18 | filepaths.extend([os.path.join(base, filename) 19 | for filename in filenames]) 20 | return {package: filepaths} 21 | 22 | 23 | setup( 24 | name='django-admin-hstore-widget', 25 | version=version, 26 | description="Widget and Field to properly render HStore data in Django-Admin", 27 | keywords="django admin hstore render", 28 | author='Alexandre Dufour', 29 | author_email='adufour@poka.io', 30 | maintainer='Alexandre Dufour', 31 | maintainer_email='adufour@poka.io', 32 | url='https://github.com/PokaInc/django-admin-hstore-widget', 33 | packages=find_packages(), 34 | package_data=get_package_data('django_admin_hstore_widget'), 35 | license='MIT', 36 | classifiers=[ 37 | 'Development Status :: 5 - Production/Stable', 38 | 'Environment :: Web Environment', 39 | 'Intended Audience :: Developers', 40 | 'License :: OSI Approved :: MIT License', 41 | 'Operating System :: OS Independent', 42 | 'Framework :: Django', 43 | 'Framework :: Django :: 1.11', 44 | 'Programming Language :: Python', 45 | 'Programming Language :: Python :: 2', 46 | 'Programming Language :: Python :: 2.7', 47 | 'Programming Language :: Python :: 3', 48 | 'Programming Language :: Python :: 3.4', 49 | 'Programming Language :: Python :: 3.5', 50 | ], 51 | zip_safe=False, 52 | ) 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PokaInc/django-admin-hstore-widget/d3438c376fb04daa664b5b31dd60937733ae375f/tests/__init__.py -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.postgres.fields import HStoreField 3 | 4 | class Cat(models.Model): 5 | name = models.CharField(max_length=32) 6 | data = HStoreField() 7 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import django 4 | 5 | 6 | DATABASES = { 7 | 'default': { 8 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 9 | 'NAME': 'django_hstore', 10 | 'USER': '', 11 | 'PASSWORD': '', 12 | 'HOST': '', 13 | 'PORT': '' 14 | }, 15 | } 16 | INSTALLED_APPS = ( 17 | 'django.contrib.auth', 18 | 'django.contrib.contenttypes', 19 | 'django.contrib.sessions', 20 | 'django.contrib.messages', 21 | 'django.contrib.staticfiles', 22 | 'django.contrib.postgres', 23 | 'django_admin_hstore_widget', 24 | 25 | 'tests', 26 | ) 27 | 28 | if django.VERSION[:2] >= (1, 7): 29 | INSTALLED_APPS = ( 30 | 'django.contrib.admin.apps.AdminConfig', 31 | ) + INSTALLED_APPS 32 | else: 33 | INSTALLED_APPS = ( 34 | 'django.contrib.admin', 35 | ) + INSTALLED_APPS 36 | 37 | MIDDLEWARE = [ 38 | 'django.middleware.security.SecurityMiddleware', 39 | 'django.contrib.sessions.middleware.SessionMiddleware', 40 | 'django.middleware.common.CommonMiddleware', 41 | 'django.middleware.csrf.CsrfViewMiddleware', 42 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 43 | 'django.contrib.messages.middleware.MessageMiddleware', 44 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 45 | ] 46 | 47 | SECRET_KEY = 'secret' 48 | 49 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 50 | DEBUG = True 51 | LANGUAGE_CODE = 'en-us' 52 | TIME_ZONE = 'UTC' 53 | USE_I18N = True 54 | USE_L10N = True 55 | USE_TZ = True 56 | STATIC_URL = '/static/' 57 | ROOT_URLCONF = 'tests.urls' 58 | 59 | ALLOWED_HOSTS = [] 60 | 61 | TEMPLATES = [ 62 | { 63 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 64 | 'DIRS': [], 65 | 'APP_DIRS': True, 66 | 'OPTIONS': { 67 | 'context_processors': [ 68 | 'django.template.context_processors.debug', 69 | 'django.template.context_processors.request', 70 | 'django.contrib.auth.context_processors.auth', 71 | 'django.contrib.messages.context_processors.messages', 72 | ], 73 | }, 74 | }, 75 | ] 76 | -------------------------------------------------------------------------------- /tests/test_hstore_field.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.urls import reverse 3 | from django.test import TestCase 4 | from django.contrib import admin 5 | from django.contrib.auth.models import User 6 | 7 | 8 | from django_admin_hstore_widget.forms import HStoreFormField 9 | 10 | from .models import Cat 11 | 12 | 13 | class CatForm(forms.ModelForm): 14 | data = HStoreFormField() 15 | 16 | class Meta: 17 | model = Cat 18 | fields = ('name', 'data') 19 | 20 | 21 | class CatAdmin(admin.ModelAdmin): 22 | form = CatForm 23 | 24 | 25 | admin.site.register(Cat, CatAdmin) 26 | 27 | 28 | class TestModel(TestCase): 29 | 30 | ADMIN_USERNAME = 'murphy' 31 | ADMIN_PASSWORD = 'cat' 32 | 33 | def _create_admin_user(self): 34 | admin = User.objects.create( 35 | username=self.ADMIN_USERNAME, 36 | is_staff=True, 37 | is_superuser=True, 38 | is_active=True 39 | ) 40 | admin.set_password(self.ADMIN_PASSWORD) 41 | admin.save() 42 | 43 | def _login(self): 44 | self.client.login(username=self.ADMIN_USERNAME, password=self.ADMIN_PASSWORD) 45 | 46 | def test__hstore_field_edit_view__render(self): 47 | self._create_admin_user() 48 | self._login() 49 | 50 | cat = Cat.objects.create(name='Murphy', data={'race': '', 'gender': 'male'}) 51 | url = reverse('admin:tests_cat_change', args=(cat.pk,)) 52 | edit_view_response = self.client.get(url) 53 | 54 | self.assertEqual(edit_view_response.status_code, 200) 55 | 56 | # If the template is rendered with the django-admin-hstore-widget this class will be added to the textarea 57 | self.assertContains(edit_view_response, 'class="hstore-original-textarea"') 58 | 59 | def test__hstore_field_add_view__render(self): 60 | self._create_admin_user() 61 | self._login() 62 | 63 | url = reverse('admin:tests_cat_add') 64 | edit_view_response = self.client.get(url) 65 | 66 | self.assertEqual(edit_view_response.status_code, 200) 67 | # If the template is rendered with the django-admin-hstore-widget this class will be added to the textarea 68 | self.assertContains(edit_view_response, 'class="hstore-original-textarea"') 69 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django import VERSION 2 | from django.conf.urls import include 3 | from django.contrib import admin 4 | 5 | if VERSION[0] < 2: 6 | from django.conf.urls import url 7 | admin.autodiscover() 8 | urlpatterns = [ 9 | url(r'^admin/', include(admin.site.urls)), 10 | ] 11 | else: 12 | from django.urls import path 13 | urlpatterns = [ 14 | path('admin/', admin.site.urls), 15 | ] 16 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | {py27,py34,py35,py36,py37}-django111, 4 | {py34,py35,py36,py37}-django20, 5 | {py35,py36,py37}-django21, 6 | {py35,py36,py37}-django22, 7 | {py36,py37}-django30, 8 | {py38}-django30, 9 | {py38}-django40, 10 | 11 | 12 | [travis:env] 13 | DJANGO = 14 | 1.11: django111 15 | 2.0: django20 16 | 2.1: django21 17 | 2.2: django22 18 | 3.0: django30 19 | 4.0: django40 20 | 21 | [latest] 22 | deps = 23 | https://github.com/django/django/archive/master.tar.gz 24 | 25 | [testenv] 26 | commands = coverage run --source django_admin_hstore_widget ./runtests.py --noinput {posargs} 27 | 28 | setenv = 29 | PYTHONDONTWRITEBYTECODE=1 30 | deps = 31 | django111: Django>=1.11,<2.0 32 | django20: Django>=2.0,<2.1 33 | django21: Django>=2.1,<2.2 34 | django22: Django>=2.2,<3.0 35 | django30: Django>=3.0,<3.1 36 | django40: Django>=4.0,<4.1 37 | -rrequirements/test-ci.txt 38 | --------------------------------------------------------------------------------