├── tests
├── __init__.py
├── testapp
│ ├── apps.py
│ └── models.py
├── urls.py
├── settings.py
├── test_fields.py
├── test_widgets.py
└── test_forms.py
├── requirements.txt
├── sample_project
├── sample_app
│ ├── __init__.py
│ ├── views.py
│ ├── migrations
│ │ ├── __init__.py
│ │ ├── 0002_auto_20190929_1805.py
│ │ └── 0001_initial.py
│ ├── apps.py
│ ├── models.py
│ └── admin.py
├── sample_project
│ ├── __init__.py
│ ├── wsgi.py
│ ├── urls.py
│ └── settings.py
├── requirements.txt
└── manage.py
├── django_better_admin_arrayfield
├── forms
│ ├── __init__.py
│ ├── widgets.py
│ └── fields.py
├── models
│ ├── __init__.py
│ └── fields.py
├── static
│ ├── img
│ │ └── .gitignore
│ ├── css
│ │ ├── django_better_admin_arrayfield.min.css
│ │ └── django_better_admin_arrayfield.css
│ └── js
│ │ ├── django_better_admin_arrayfield.min.js
│ │ └── django_better_admin_arrayfield.js
├── __init__.py
├── apps.py
├── admin
│ └── mixins.py
├── templates
│ └── django_better_admin_arrayfield
│ │ └── forms
│ │ └── widgets
│ │ └── dynamic_array.html
└── locale
│ ├── es
│ └── LC_MESSAGES
│ │ └── django.po
│ ├── fr
│ └── LC_MESSAGES
│ │ └── django.po
│ └── pt-br
│ └── LC_MESSAGES
│ └── django.po
├── readme_images
├── after.png
└── before.jpg
├── requirements_dev.txt
├── .pre-commit-config.yaml
├── .coveragerc
├── requirements_test.txt
├── docker-compose.yml
├── MANIFEST.in
├── manage.py
├── .editorconfig
├── .github
├── ISSUE_TEMPLATE.md
└── workflows
│ └── tests.yml
├── setup.cfg
├── .gitignore
├── pyproject.toml
├── tox.ini
├── AUTHORS.md
├── LICENSE
├── HISTORY.md
├── Makefile
├── setup.py
├── CONTRIBUTING.md
└── README.md
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | django>=2.0,<3.2
2 |
--------------------------------------------------------------------------------
/sample_project/sample_app/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sample_project/sample_app/views.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sample_project/sample_project/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/forms/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sample_project/sample_app/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/models/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/static/img/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sample_project/requirements.txt:
--------------------------------------------------------------------------------
1 | Django==3.1.7
2 | -e ../
3 | psycopg2-binary==2.8.6
4 |
--------------------------------------------------------------------------------
/readme_images/after.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gradam/django-better-admin-arrayfield/HEAD/readme_images/after.png
--------------------------------------------------------------------------------
/readme_images/before.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gradam/django-better-admin-arrayfield/HEAD/readme_images/before.jpg
--------------------------------------------------------------------------------
/requirements_dev.txt:
--------------------------------------------------------------------------------
1 | bumpversion==0.6.0
2 | wheel==0.36.2
3 | pre-commit==2.11.1
4 | black==20.8b1
5 | psycopg2-binary==2.8.6
6 |
--------------------------------------------------------------------------------
/sample_project/sample_app/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class SampleAppConfig(AppConfig):
5 | name = "sample_app"
6 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/ambv/black
3 | rev: 19.10b0
4 | hooks:
5 | - id: black
6 | language_version: python3.7
7 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "1.4.2"
2 |
3 | default_app_config = "django_better_admin_arrayfield.apps.DjangoBetterAdminArrayfieldConfig"
4 |
--------------------------------------------------------------------------------
/tests/testapp/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class TestAppConfig(AppConfig):
5 | name = "tests.testapp"
6 | verbose_name = "TestApp"
7 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | branch = true
3 |
4 | [report]
5 | omit =
6 | *site-packages*
7 | *tests*
8 | *.tox*
9 | show_missing = True
10 | exclude_lines =
11 | raise NotImplementedError
12 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8
2 | from django.apps import AppConfig
3 |
4 |
5 | class DjangoBetterAdminArrayfieldConfig(AppConfig):
6 | name = "django_better_admin_arrayfield"
7 |
--------------------------------------------------------------------------------
/requirements_test.txt:
--------------------------------------------------------------------------------
1 | coverage==5.5
2 | flake8==3.9.0
3 | tox==3.23.0
4 | codecov==2.1.11
5 | pytest==6.1.2
6 | pytest-django==4.1.0
7 | pytest-cov==2.11.1
8 | pytest-mock==3.5.1
9 | psycopg2-binary==2.8.6
10 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/admin/mixins.py:
--------------------------------------------------------------------------------
1 | class DynamicArrayMixin:
2 | class Media:
3 | js = ("js/django_better_admin_arrayfield.min.js",)
4 | css = {"all": ("css/django_better_admin_arrayfield.min.css",)}
5 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | db:
5 | image: postgres:latest
6 | environment:
7 | POSTGRES_USER: sample_user
8 | POSTGRES_PASSWORD: 12345
9 | POSTGRES_DB: django_db
10 | ports:
11 | - "5436:5432"
12 |
--------------------------------------------------------------------------------
/sample_project/sample_app/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from django_better_admin_arrayfield.models.fields import ArrayField
4 |
5 |
6 | class ArrayModel(models.Model):
7 | sample_array = ArrayField(models.CharField(max_length=20), blank=True, null=True)
8 |
--------------------------------------------------------------------------------
/tests/urls.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import, unicode_literals
3 |
4 | from django.conf.urls import include, url
5 |
6 | urlpatterns = [url(r"^", include("django_better_admin_arrayfield.urls", namespace="django_better_admin_arrayfield"))]
7 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include AUTHORS.md
2 | include CONTRIBUTING.md
3 | include HISTORY.md
4 | include LICENSE
5 | include README.md
6 | recursive-include django_better_admin_arrayfield *.html *.png *.gif *.js *.min.js *.css *.min.css *jpg *jpeg *svg *py
7 | recursive-include django_better_admin_arrayfield/locale *.po *.mo
8 |
--------------------------------------------------------------------------------
/sample_project/sample_app/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
4 | from sample_app.models import ArrayModel
5 |
6 |
7 | class ArrayModelAdmin(admin.ModelAdmin, DynamicArrayMixin):
8 | pass
9 |
10 |
11 | admin.site.register(ArrayModel, ArrayModelAdmin)
12 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/models/fields.py:
--------------------------------------------------------------------------------
1 | from django.contrib.postgres.fields import ArrayField as DjangoArrayField
2 |
3 | from django_better_admin_arrayfield.forms.fields import DynamicArrayField
4 |
5 |
6 | class ArrayField(DjangoArrayField):
7 | def formfield(self, **kwargs):
8 | return super().formfield(**{"form_class": DynamicArrayField, **kwargs})
9 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | from __future__ import unicode_literals, absolute_import
4 |
5 | import os
6 | import sys
7 |
8 | if __name__ == "__main__":
9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
10 | from django.core.management import execute_from_command_line
11 |
12 | execute_from_command_line(sys.argv)
13 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.{py,rst,ini}]
12 | indent_style = space
13 | indent_size = 4
14 |
15 | [*.{html,css,scss,json,yml}]
16 | indent_style = space
17 | indent_size = 2
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
22 | [Makefile]
23 | indent_style = tab
24 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/static/css/django_better_admin_arrayfield.min.css:
--------------------------------------------------------------------------------
1 | .dynamic-array-widget .array-item{display:flex;align-items:center}.dynamic-array-widget .remove{height:15px;display:flex;align-items:center;margin-left:5px}.dynamic-array-widget .remove:hover{cursor:pointer}.dynamic-array-widget button{height:20px;background:#79aec8;border-radius:4px;color:#fff;cursor:pointer;border:none}.dynamic-array-widget button:hover{background:#609ab6}
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | * Django better admin ArrayField version:
2 | * Django version:
3 | * Python version:
4 | * Operating System:
5 |
6 | ### Description
7 |
8 | Describe what you were trying to get done.
9 | Tell us what happened, what went wrong, and what you expected to happen.
10 |
11 | ### What I Did
12 |
13 | ```
14 | Paste the command(s) you ran and the output.
15 | If there was a crash, please include the traceback here.
16 | ```
17 |
--------------------------------------------------------------------------------
/sample_project/sample_project/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for sample_project project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sample_project.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 1.4.2
3 | commit = True
4 | tag = True
5 |
6 | [bumpversion:file:setup.py]
7 |
8 | [bumpversion:file:django_better_admin_arrayfield/__init__.py]
9 |
10 | [wheel]
11 | universal = 1
12 |
13 | [flake8]
14 | max-complexity = 18
15 | select = B,C,E,F,W,T4,B9
16 | ignore = E203, E266, E501, W503
17 | exclude =
18 | django_better_admin_arrayfield/migrations,
19 | .git,
20 | .tox,
21 | .venv,
22 | docs/conf.py,
23 | build,
24 | dist
25 | max-line-length = 119
26 |
--------------------------------------------------------------------------------
/tests/testapp/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from django_better_admin_arrayfield.models.fields import ArrayField
4 |
5 |
6 | class DefaultValueNullableModel(models.Model):
7 | array = ArrayField(models.IntegerField(), blank=True, null=True, default=list)
8 |
9 |
10 | class DefaultValueRequiredModel(models.Model):
11 | array = ArrayField(models.IntegerField(), blank=True, default=list)
12 |
13 |
14 | class NullableNoDefaultModel(models.Model):
15 | array = ArrayField(models.IntegerField(), blank=True, null=True)
16 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/static/css/django_better_admin_arrayfield.css:
--------------------------------------------------------------------------------
1 | .dynamic-array-widget .array-item {
2 | display: flex;
3 | align-items: center;
4 | }
5 |
6 | .dynamic-array-widget .remove {
7 | height: 15px;
8 | display: flex;
9 | align-items: center;
10 | margin-left: 5px;
11 | }
12 |
13 | .dynamic-array-widget .remove:hover {
14 | cursor: pointer;
15 | }
16 |
17 | .dynamic-array-widget button {
18 | height: 20px;
19 | background: #79aec8;
20 | border-radius: 4px;
21 | color: #fff;
22 | cursor: pointer;
23 | border: none;
24 | }
25 |
26 | .dynamic-array-widget button:hover {
27 | background: #609ab6;
28 | }
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 | __pycache__
3 | .venv/
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Packages
9 | *.egg
10 | *.egg-info
11 | dist
12 | build
13 | eggs
14 | parts
15 | bin
16 | var
17 | sdist
18 | develop-eggs
19 | .installed.cfg
20 | lib
21 | lib64
22 |
23 | # Installer logs
24 | pip-log.txt
25 |
26 | # Unit test / coverage reports
27 | .coverage
28 | .tox
29 | nosetests.xml
30 | htmlcov
31 |
32 | # Translations
33 | *.mo
34 |
35 | # Mr Developer
36 | .mr.developer.cfg
37 | .project
38 | .pydevproject
39 |
40 | # Pycharm/Intellij
41 | .idea
42 |
43 | # Complexity
44 | output/*.html
45 | output/*/index.html
46 |
47 | # Sphinx
48 | docs/_build
49 |
50 | # Pipenv
51 | Pipfile
52 | Pipfile.lock
53 |
--------------------------------------------------------------------------------
/sample_project/sample_app/migrations/0002_auto_20190929_1805.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.5 on 2019-09-29 18:05
2 |
3 | from django.db import migrations, models
4 |
5 | import django_better_admin_arrayfield.models.fields
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ("sample_app", "0001_initial"),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name="arraymodel",
17 | name="sample_array",
18 | field=django_better_admin_arrayfield.models.fields.ArrayField(
19 | base_field=models.CharField(max_length=20), blank=True, null=True, size=None
20 | ),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/tests/settings.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8
2 | DEBUG = True
3 | USE_TZ = True
4 |
5 | # SECURITY WARNING: keep the secret key used in production secret!
6 | SECRET_KEY = "w7r3dc3#mcs2e5cs7!vb_647eng4dw6i2io+k9k6#solz63i$_"
7 |
8 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}
9 |
10 | ROOT_URLCONF = "tests.urls"
11 |
12 | INSTALLED_APPS = [
13 | "django.contrib.auth",
14 | "django.contrib.contenttypes",
15 | "django.contrib.sites",
16 | "django_better_admin_arrayfield",
17 | "tests.testapp.apps.TestAppConfig",
18 | ]
19 |
20 | TEMPLATES = [{"BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True}]
21 |
22 | SITE_ID = 1
23 |
24 | MIDDLEWARE = ()
25 |
--------------------------------------------------------------------------------
/sample_project/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sample_project.settings")
9 | try:
10 | from django.core.management import execute_from_command_line
11 | except ImportError as exc:
12 | raise ImportError(
13 | "Couldn't import Django. Are you sure it's installed and "
14 | "available on your PYTHONPATH environment variable? Did you "
15 | "forget to activate a virtual environment?"
16 | ) from exc
17 | execute_from_command_line(sys.argv)
18 |
19 |
20 | if __name__ == "__main__":
21 | main()
22 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | strategy:
15 | max-parallel: 4
16 | matrix:
17 | python-version: [3.5, 3.6, 3.7, 3.8]
18 |
19 | steps:
20 | - uses: actions/checkout@v1
21 | - name: Set up Python ${{ matrix.python-version }}
22 | uses: actions/setup-python@v1
23 | with:
24 | python-version: ${{ matrix.python-version }}
25 | - name: Install dependencies
26 | run: |
27 | python -m pip install --upgrade pip
28 | pip install tox tox-gh-actions
29 | - name: Test with tox
30 | run: tox
31 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.black]
2 | line-length = 119
3 | py36 = true
4 | include = '\.pyi?$'
5 | exclude = '''
6 | /(
7 | \.git
8 | | \.hg
9 | | \.mypy_cache
10 | | \.tox
11 | | \.venv
12 | | \.pytest_cache
13 | | migrations
14 | | _build
15 | | buck-out
16 | | build
17 | | dist
18 | | docs/conf.py
19 | | build
20 | )/
21 | '''
22 |
23 | [tool.pytest.ini_options]
24 | DJANGO_SETTINGS_MODULE = "tests.settings"
25 | django_find_project = false
26 |
27 | [tool.isort]
28 | line_length = 120
29 | multi_line_output = 3
30 | include_trailing_comma = true
31 | known_django = ["django"]
32 | known_first_party = ["sample_app"]
33 | sections = ["FUTURE","STDLIB","DJANGO","THIRDPARTY","FIRSTPARTY","LOCALFOLDER"]
34 | known_third_party = ["pytest"]
35 |
--------------------------------------------------------------------------------
/sample_project/sample_project/urls.py:
--------------------------------------------------------------------------------
1 | """sample_project URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.contrib import admin
17 | from django.urls import path
18 |
19 | urlpatterns = [path("admin/", admin.site.urls)]
20 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist =
3 | django{2,21,22,30,31}-{py35,py36,py37,py38},
4 | lint
5 |
6 | [gh-actions]
7 | python =
8 | 3.5: py35
9 | 3.6: py36
10 | 3.7: py37
11 | 3.8: py38, lint
12 |
13 | [testenv]
14 | deps =
15 | django2: Django<2.1
16 | django21: Django<2.2
17 | django22: Django<3.0
18 | django30: Django<3.1
19 | django31: Django<3.2
20 | -r{toxinidir}/requirements_test.txt
21 | setenv =
22 | PYTHONPATH = {toxinidir}:{toxinidir}/django_better_admin_arrayfield
23 | commands = pytest --cov=django_better_admin_arrayfield tests/
24 |
25 | [testenv:lint]
26 | commands =
27 | isort --check django_better_admin_arrayfield tests sample_project
28 | black --check --diff django_better_admin_arrayfield tests sample_project
29 | flake8
30 | deps =
31 | isort
32 | flake8
33 | black
34 |
--------------------------------------------------------------------------------
/sample_project/sample_app/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.5 on 2019-09-29 18:03
2 |
3 | from django.db import migrations, models
4 |
5 | import django_better_admin_arrayfield.models.fields
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = []
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name="ArrayModel",
17 | fields=[
18 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
19 | (
20 | "sample_array",
21 | django_better_admin_arrayfield.models.fields.ArrayField(
22 | base_field=models.CharField(max_length=20), size=None
23 | ),
24 | ),
25 | ],
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/static/js/django_better_admin_arrayfield.min.js:
--------------------------------------------------------------------------------
1 | window.addEventListener("load",function(){function a(a){a.querySelectorAll(".remove").forEach(a=>{a.addEventListener("click",()=>{a.parentNode.remove()})})}function b(b){const d=b.querySelector(".array-item"),e=d.cloneNode(!0),f=d.parentElement;d.getAttribute("data-isNone")&&(d.remove(),e.removeAttribute("data-isNone"),e.removeAttribute("style")),a(b),b.querySelector(".add-array-item").addEventListener("click",()=>{c++;const b=e.cloneNode(!0),d=b.querySelector("input").getAttribute("id").split("_"),g=d.slice(0,-1).join("_")+"_"+(c-1+"");b.querySelector("input").setAttribute("id",g),b.querySelector("input").value="",a(b),f.appendChild(b)})}let c=1;django.jQuery(".dynamic-array-widget").not(".empty-form .dynamic-array-widget").each((a,c)=>b(c)),django.jQuery(document).on("formset:added",function(a,c){c[0].querySelectorAll(".dynamic-array-widget").forEach(a=>b(a))})});
2 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/templates/django_better_admin_arrayfield/forms/widgets/dynamic_array.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load i18n %}
3 |
4 | {% spaceless %}
5 |
22 | {% endspaceless %}
23 |
--------------------------------------------------------------------------------
/AUTHORS.md:
--------------------------------------------------------------------------------
1 | # Credits
2 |
3 | ## Development Lead
4 |
5 | - Jakub Semik \<\>
6 |
7 | ## Contributors
8 |
9 | | Full Name | GitHub |
10 | | ------ | ------ |
11 | | Davit Tovmasyan | [@davitovmasyan]( https://github.com/davitovmasyan ) |
12 | | Sergei Maertens | [@sergei-maertens]( https://github.com/sergei-maertens ) |
13 | | 李琼羽 | [@w392807287]( https://github.com/w392807287 ) |
14 | | Usman Hussain | [@usmandap]( https://github.com/usmandap ) |
15 | | Wojciech Semik | [@paterit]( https://github.com/paterit ) |
16 | | Jules Robichaud-Gagnon | [@jrobichaud]( https://github.com/jrobichaud ) |
17 | | Dustin Martin | [@dmartin]( https://github.com/dmartin ) |
18 | | Davit Tovmasyan | [@davitovmasyan]( https://github.com/davitovmasyan ) |
19 | | Swojak-A | [@Swojak-A]( https://github.com/Swojak-A ) |
20 | | Álvaro Mondéjar | [@mondeja]( https://github.com/mondeja ) |
21 | | Martín | [@MartinCura]( https://github.com/MartinCura ) |
22 | | Joshua Cender | [@sixthgear]( https://github.com/sixthgear ) |
23 | | Javier Matos Odut | [@javiermatos]( https://github.com/javiermatos )
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | MIT License
3 |
4 | Copyright (c) 2019, Jakub Semik
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9 |
10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/locale/es/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # Spanish translation of django_better_admin_arrayfield.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the django_better_admin_arrayfield package.
4 | # Álvaro Mondéjar Rubio , 2020.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: 1.0.0\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2020-04-27 10:01+0200\n"
12 | "PO-Revision-Date: 2020-04-23 18:52-0015\n"
13 | "Last-Translator: b' '\n"
14 | "Language-Team: LANGUAGE \n"
15 | "Language: es\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
20 | "X-Translated-Using: django-rosetta 0.9.4\n"
21 |
22 | #: django_better_admin_arrayfield/forms/fields.py:13
23 | #, python-format
24 | msgid "Item %(nth)s in the array did not validate: "
25 | msgstr "Element %(nth)sº en el array no válido: "
26 |
27 | #: django_better_admin_arrayfield/templates/django_better_admin_arrayfield/forms/widgets/dynamic_array.html:16
28 | msgid "Add another"
29 | msgstr "Añadir otro"
30 |
--------------------------------------------------------------------------------
/tests/test_fields.py:
--------------------------------------------------------------------------------
1 | from django.forms import ValidationError
2 | from django.forms.fields import CharField
3 |
4 | import pytest
5 |
6 | from django_better_admin_arrayfield.forms.fields import DynamicArrayField
7 |
8 |
9 | def test_field_not_required():
10 | field = DynamicArrayField(CharField(max_length=10), required=False)
11 | data = []
12 | field.clean(data)
13 | data = ["12", "13"]
14 | field.clean(data)
15 |
16 |
17 | def test_field_required():
18 | field = DynamicArrayField(CharField(max_length=10), required=True)
19 | data = []
20 | with pytest.raises(ValidationError):
21 | field.clean(data)
22 | data = ["12", "13"]
23 | field.clean(data)
24 |
25 |
26 | def test_default():
27 | default = ["1"]
28 | field = DynamicArrayField(CharField(max_length=10), required=True, default=default)
29 | data = []
30 | cleaned_data = field.clean(data)
31 | assert cleaned_data == default
32 |
33 |
34 | def test_callable_default():
35 | def default():
36 | return ["1", "2"]
37 |
38 | field = DynamicArrayField(CharField(max_length=10), required=True, default=default)
39 | data = []
40 | cleaned_data = field.clean(data)
41 | assert cleaned_data == default()
42 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/locale/fr/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # French translation of django_better_admin_arrayfield.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the django_better_admin_arrayfield package.
4 | # Marcelo Cardoso , 2020.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: 1.0.0\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2020-04-27 10:01+0200\n"
12 | "PO-Revision-Date: 2020-04-23 18:52-0015\n"
13 | "Last-Translator: b' '\n"
14 | "Language-Team: LANGUAGE \n"
15 | "Language: fr\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
20 | "X-Translated-Using: django-rosetta 0.9.4\n"
21 |
22 | #: django_better_admin_arrayfield/forms/fields.py:13
23 | #, python-format
24 | msgid "Item %(nth)s in the array did not validate: "
25 | msgstr "L'élément %(nth)sº de l'array n'est pas valide: "
26 |
27 | #: django_better_admin_arrayfield/templates/django_better_admin_arrayfield/forms/widgets/dynamic_array.html:16
28 | msgid "Add another"
29 | msgstr "Ajouter un autre"
30 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/locale/pt-br/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # Brazilian portuguese translation of django_better_admin_arrayfield.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the django_better_admin_arrayfield package.
4 | # Marcelo Cardoso , 2020.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: 1.0.0\n"
10 | "Report-Msgid-Bugs-To: \n"
11 | "POT-Creation-Date: 2020-04-27 10:01+0200\n"
12 | "PO-Revision-Date: 2020-04-23 18:52-0015\n"
13 | "Last-Translator: b' '\n"
14 | "Language-Team: LANGUAGE \n"
15 | "Language: pt-br\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=UTF-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n"
20 | "X-Translated-Using: django-rosetta 0.9.4\n"
21 |
22 | #: django_better_admin_arrayfield/forms/fields.py:13
23 | #, python-format
24 | msgid "Item %(nth)s in the array did not validate: "
25 | msgstr "Elemento %(nth)sº no array não é válido: "
26 |
27 | #: django_better_admin_arrayfield/templates/django_better_admin_arrayfield/forms/widgets/dynamic_array.html:16
28 | msgid "Add another"
29 | msgstr "Adicionar outro"
30 |
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | # History
2 |
3 | ## 1.4.2 (2020-12-08)
4 |
5 | - Adjust template to better match django style
6 |
7 | ## 1.4.1 (2020-12-08)
8 |
9 | - Allow submitting empty array field
10 |
11 | ## 1.4.0 (2020-10-04)
12 |
13 | - allow choosing subwidget for DynamicArrayWidget
14 |
15 | ## 1.3.0 (2020-07-09)
16 |
17 | - Handle default values in form field
18 |
19 | ## 1.2.1 (2020-07-09)
20 |
21 | - Fix tests requirements
22 |
23 | ## 1.2.0 (2020-07-09)
24 |
25 | - handle default values in model fields
26 |
27 | ## 1.1.0 (2020-04-28)
28 |
29 | - Add spanish translations
30 |
31 | ## 1.0.7 (2020-04-27)
32 |
33 | - Add possibility to i18n strings
34 |
35 | ## 1.0.6 (2020-04-15)
36 |
37 | - Remove debugging print statements
38 | - use default_app_config for easier integration
39 | - Support dynamically-added inline forms
40 |
41 | ## 1.0.5 (2019-12-30)
42 |
43 | - Add python 3.8 and Django 3.0 to tests
44 |
45 | ## 1.0.4 (2019-09-02)
46 |
47 | - Can add item after removing everything from the list
48 |
49 | ## 1.0.3 (2019-09-02)
50 |
51 | - Can add item after removing everything from the list
52 | - Do not call static at startup time
53 |
54 | ## 1.0.2 (2019-04-03)
55 |
56 | - If field is required empty list raises ValidationError on clean.
57 |
58 | ## 1.0.1 (2019-02-23)
59 |
60 | - Empty list is no longer recognized as changed.
61 |
62 |
63 | ## 1.0.0 (2019-02-21)
64 |
65 | - First release on PyPI.
66 |
--------------------------------------------------------------------------------
/tests/test_widgets.py:
--------------------------------------------------------------------------------
1 | from django.template.loader import get_template
2 |
3 | from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
4 |
5 |
6 | def test_template_exists():
7 | get_template(DynamicArrayWidget.template_name)
8 |
9 |
10 | def test_format_value():
11 | widget = DynamicArrayWidget()
12 | assert widget.format_value("") == []
13 | assert widget.format_value([1, 2, 3]) == [1, 2, 3]
14 |
15 |
16 | def test_value_from_datadict(mocker):
17 | widget = DynamicArrayWidget()
18 | test_name = "name"
19 |
20 | class MockData:
21 | @staticmethod
22 | def getlist(name):
23 | return [name]
24 |
25 | mocker.spy(MockData, "getlist")
26 |
27 | assert widget.value_from_datadict(MockData, None, test_name) == [test_name]
28 | assert MockData.getlist.call_count == 1
29 |
30 |
31 | def test_value_from_datadict_get(mocker):
32 | widget = DynamicArrayWidget()
33 | test_name = "name"
34 |
35 | class MockData:
36 | @staticmethod
37 | def get(name):
38 | return name
39 |
40 | mocker.spy(MockData, "get")
41 |
42 | assert widget.value_from_datadict(MockData, None, test_name) == test_name
43 | assert MockData.get.call_count == 1
44 |
45 |
46 | def test_get_context():
47 | widget = DynamicArrayWidget()
48 | value = ["1", "2", "3"]
49 |
50 | context = widget.get_context("name", value, [])
51 | assert len(context["widget"]["subwidgets"]) == len(value)
52 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean-pyc clean-build docs help
2 | .DEFAULT_GOAL := help
3 | define BROWSER_PYSCRIPT
4 | import os, webbrowser, sys
5 | try:
6 | from urllib import pathname2url
7 | except:
8 | from urllib.request import pathname2url
9 |
10 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
11 | endef
12 | export BROWSER_PYSCRIPT
13 | BROWSER := python -c "$$BROWSER_PYSCRIPT"
14 |
15 | help:
16 | @grep '^[a-zA-Z]' $(MAKEFILE_LIST) | sort | awk -F ':.*?## ' 'NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}'
17 |
18 | clean: clean-build clean-pyc
19 |
20 | clean-build: ## remove build artifacts
21 | rm -fr build/
22 | rm -fr dist/
23 | rm -fr *.egg-info
24 |
25 | clean-pyc: ## remove Python file artifacts
26 | find . -name '*.pyc' -exec rm -f {} +
27 | find . -name '*.pyo' -exec rm -f {} +
28 | find . -name '*~' -exec rm -f {} +
29 |
30 | lint: ## check style with flake8
31 | flake8 django_better_admin_arrayfield tests
32 |
33 | test: ## run tests quickly with the default Python
34 | python runtests.py tests
35 |
36 | test-all: ## run tests on every Python version with tox
37 | tox
38 |
39 | coverage: ## check code coverage quickly with the default Python
40 | coverage run --source django_better_admin_arrayfield runtests.py tests
41 | coverage report -m
42 | coverage html
43 | open htmlcov/index.html
44 |
45 | docs: ## generate Sphinx HTML documentation, including API docs
46 | rm -f docs/django-better-admin-arrayfield.rst
47 | rm -f docs/modules.rst
48 | sphinx-apidoc -o docs/ django_better_admin_arrayfield
49 | $(MAKE) -C docs clean
50 | $(MAKE) -C docs html
51 | $(BROWSER) docs/_build/html/index.html
52 |
53 | release: clean ## package and upload a release
54 | python setup.py sdist bdist_wheel
55 | twine upload dist/*
56 |
57 | sdist: clean ## package
58 | python setup.py sdist bdist_wheel
59 | ls -l dist
60 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/forms/widgets.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 |
4 | class DynamicArrayWidget(forms.TextInput):
5 |
6 | template_name = "django_better_admin_arrayfield/forms/widgets/dynamic_array.html"
7 |
8 | def __init__(self, *args, **kwargs):
9 | self.subwidget_form = kwargs.pop("subwidget_form", forms.TextInput)
10 | super().__init__(*args, **kwargs)
11 |
12 | def get_context(self, name, value, attrs):
13 | context_value = value or [""]
14 | context = super().get_context(name, context_value, attrs)
15 | final_attrs = context["widget"]["attrs"]
16 | id_ = context["widget"]["attrs"].get("id")
17 | context["widget"]["is_none"] = value is None
18 |
19 | subwidgets = []
20 | for index, item in enumerate(context["widget"]["value"]):
21 | widget_attrs = final_attrs.copy()
22 | if id_:
23 | widget_attrs["id"] = "{id_}_{index}".format(id_=id_, index=index)
24 | widget = self.subwidget_form()
25 | widget.is_required = self.is_required
26 | subwidgets.append(widget.get_context(name, item, widget_attrs)["widget"])
27 |
28 | context["widget"]["subwidgets"] = subwidgets
29 | return context
30 |
31 | def value_from_datadict(self, data, files, name):
32 | try:
33 | getter = data.getlist
34 | return [value for value in getter(name) if value]
35 | except AttributeError:
36 | return data.get(name)
37 |
38 | def value_omitted_from_data(self, data, files, name):
39 | return False
40 |
41 | def format_value(self, value):
42 | return value or []
43 |
44 |
45 | class DynamicArrayTextareaWidget(DynamicArrayWidget):
46 | def __init__(self, *args, **kwargs):
47 | kwargs.setdefault("subwidget_form", forms.Textarea)
48 | super().__init__(*args, **kwargs)
49 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/forms/fields.py:
--------------------------------------------------------------------------------
1 | from itertools import chain
2 |
3 | from django import forms
4 | from django.contrib.postgres.utils import prefix_validation_error
5 | from django.utils.translation import gettext_lazy as _
6 |
7 | from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
8 |
9 |
10 | class DynamicArrayField(forms.Field):
11 |
12 | default_error_messages = {
13 | "item_invalid": _("Item %(nth)s in the array did not validate: "),
14 | }
15 |
16 | def __init__(self, base_field, **kwargs):
17 | self.base_field = base_field
18 | self.max_length = kwargs.pop("max_length", None)
19 | self.default = kwargs.pop("default", None)
20 | kwargs.setdefault("widget", DynamicArrayWidget)
21 | super().__init__(**kwargs)
22 |
23 | def clean(self, value):
24 | cleaned_data = []
25 | errors = []
26 | if value is not None:
27 | value = [x for x in value if x]
28 |
29 | for index, item in enumerate(value):
30 | try:
31 | cleaned_data.append(self.base_field.clean(item))
32 | except forms.ValidationError as error:
33 | errors.append(
34 | prefix_validation_error(
35 | error, self.error_messages["item_invalid"], code="item_invalid", params={"nth": index}
36 | )
37 | )
38 |
39 | if not value:
40 | cleaned_data = self.default() if callable(self.default) else self.default
41 | if cleaned_data is None and self.initial is not None:
42 | cleaned_data = self.initial() if callable(self.initial) else self.initial
43 | if errors:
44 | raise forms.ValidationError(list(chain.from_iterable(errors)))
45 | if not cleaned_data and self.required:
46 | raise forms.ValidationError(self.error_messages["required"])
47 |
48 | return cleaned_data
49 |
50 | def has_changed(self, initial, data):
51 | if not data and not initial:
52 | return False
53 | return super().has_changed(initial, data)
54 |
--------------------------------------------------------------------------------
/django_better_admin_arrayfield/static/js/django_better_admin_arrayfield.js:
--------------------------------------------------------------------------------
1 | // I am to lazy to add js transpiler and compressor to this. just use https://jscompress.com
2 |
3 | window.addEventListener('load', function () {
4 | let item_count = 1;
5 |
6 | function addRemoveEventListener(widgetElement) {
7 | widgetElement.querySelectorAll('.remove').forEach(element => {
8 | element.addEventListener('click', () => {
9 | element.parentNode.remove();
10 | });
11 | });
12 | }
13 |
14 | function initializeWidget(widgetElement) {
15 | const initialElement = widgetElement.querySelector('.array-item');
16 | const elementTemplate = initialElement.cloneNode(true);
17 | const parentElement = initialElement.parentElement;
18 |
19 | if (initialElement.getAttribute('data-isNone')) {
20 | initialElement.remove();
21 | elementTemplate.removeAttribute('data-isNone');
22 | elementTemplate.removeAttribute('style');
23 | }
24 | addRemoveEventListener(widgetElement);
25 |
26 | widgetElement.querySelector('.add-array-item').addEventListener('click', () => {
27 | item_count++;
28 | const newElement = elementTemplate.cloneNode(true);
29 | const id_parts = newElement.querySelector('input').getAttribute('id').split('_');
30 | const id = id_parts.slice(0, -1).join('_') + '_' + String(item_count - 1);
31 | newElement.querySelector('input').setAttribute('id', id);
32 | newElement.querySelector('input').value = '';
33 |
34 | addRemoveEventListener(newElement);
35 | parentElement.appendChild(newElement);
36 | });
37 | }
38 |
39 | django.jQuery(".dynamic-array-widget").not(".empty-form .dynamic-array-widget").each(
40 | (index, widgetElement) => initializeWidget(widgetElement)
41 | );
42 |
43 | django.jQuery(document).on('formset:added', function(event, $row, formsetName) {
44 | $row[0].querySelectorAll(".dynamic-array-widget").forEach(
45 | widgetElement => initializeWidget(widgetElement)
46 | );
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/tests/test_forms.py:
--------------------------------------------------------------------------------
1 | from typing import Type
2 |
3 | from django.forms import IntegerField, ModelForm
4 |
5 | import pytest
6 |
7 | from django_better_admin_arrayfield.forms.fields import DynamicArrayField
8 | from tests.testapp.models import DefaultValueNullableModel, DefaultValueRequiredModel, NullableNoDefaultModel
9 |
10 |
11 | def form_factory(model, **attrs) -> Type[ModelForm]:
12 | meta = type("Meta", (), {"model": model, "fields": ["array"]})
13 | return type("SampleForm", (ModelForm,), {"Meta": meta, **attrs})
14 |
15 |
16 | class TestDefaultValue:
17 | @pytest.mark.parametrize(
18 | "model_class,init_value,expected_value",
19 | (
20 | (DefaultValueNullableModel, None, []),
21 | (NullableNoDefaultModel, None, None),
22 | (DefaultValueRequiredModel, None, []),
23 | (DefaultValueNullableModel, [1], [1]),
24 | (NullableNoDefaultModel, [1], [1]),
25 | (DefaultValueRequiredModel, [1], [1]),
26 | ),
27 | )
28 | def test_default_list(self, model_class, init_value, expected_value):
29 | data = {}
30 | if init_value is not None:
31 | data["array"] = init_value
32 |
33 | form = form_factory(model_class)(data=data)
34 | assert form.is_valid()
35 | assert form.cleaned_data["array"] == expected_value
36 |
37 |
38 | class TestFormDefaultField:
39 | @pytest.mark.parametrize(
40 | "model_class,init_value,expected_value",
41 | (
42 | (DefaultValueNullableModel, None, [1]),
43 | (NullableNoDefaultModel, None, [1]),
44 | (DefaultValueRequiredModel, None, [1]),
45 | (DefaultValueNullableModel, [2], [2]),
46 | (NullableNoDefaultModel, [2], [2]),
47 | (DefaultValueRequiredModel, [2], [2]),
48 | ),
49 | )
50 | def test_default_different_values(self, model_class, init_value, expected_value):
51 | default = [1]
52 | data = {}
53 | if init_value is not None:
54 | data["array"] = init_value
55 |
56 | field = DynamicArrayField(IntegerField(), required=True, default=default)
57 |
58 | form = form_factory(model_class, array=field)(data=data)
59 | assert form.is_valid()
60 | assert form.cleaned_data["array"] == expected_value
61 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | import os
4 | import re
5 | import sys
6 |
7 | try:
8 | from setuptools import setup
9 | except ImportError:
10 | from distutils.core import setup
11 |
12 |
13 | def get_version(*file_paths):
14 | """Retrieves the version from django_better_admin_arrayfield/__init__.py"""
15 | filename = os.path.join(os.path.dirname(__file__), *file_paths)
16 | version_file = open(filename).read()
17 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
18 | if version_match:
19 | return version_match.group(1)
20 | raise RuntimeError("Unable to find version string.")
21 |
22 |
23 | version = get_version("django_better_admin_arrayfield", "__init__.py")
24 |
25 |
26 | if sys.argv[-1] == "publish":
27 | try:
28 | import wheel
29 |
30 | print("Wheel version: ", wheel.__version__)
31 | except ImportError:
32 | print('Wheel library missing. Please run "pip install wheel"')
33 | sys.exit()
34 | os.system("python setup.py sdist upload")
35 | os.system("python setup.py bdist_wheel upload")
36 | sys.exit()
37 |
38 | if sys.argv[-1] == "tag":
39 | print("Tagging the version on git:")
40 | os.system("git tag -a %s -m 'version %s'" % (version, version))
41 | os.system("git push --tags")
42 | sys.exit()
43 |
44 | with open("README.md") as f:
45 | readme = f.read()
46 | with open("HISTORY.md") as f:
47 | history = f.read().replace(".. :changelog:", "")
48 |
49 | setup(
50 | name="django-better-admin-arrayfield",
51 | version=version,
52 | description="""Better ArrayField widget for admin""",
53 | long_description=readme + "\n\n" + history,
54 | long_description_content_type="text/markdown",
55 | author="Jakub Semik",
56 | author_email="kuba.semik@gmail.com",
57 | url="https://github.com/gradam/django-better-admin-arrayfield",
58 | packages=["django_better_admin_arrayfield"],
59 | include_package_data=True,
60 | install_requires=[],
61 | license="MIT",
62 | zip_safe=False,
63 | keywords="django-better-admin-arrayfield",
64 | classifiers=[
65 | "Development Status :: 4 - Beta",
66 | "Framework :: Django :: 3.1",
67 | "Framework :: Django :: 3.0",
68 | "Framework :: Django :: 2.2",
69 | "Framework :: Django :: 2.1",
70 | "Framework :: Django :: 2.0",
71 | "Intended Audience :: Developers",
72 | "License :: OSI Approved :: BSD License",
73 | "Natural Language :: English",
74 | "Programming Language :: Python :: 3",
75 | "Programming Language :: Python :: 3.5",
76 | "Programming Language :: Python :: 3.6",
77 | "Programming Language :: Python :: 3.7",
78 | "Programming Language :: Python :: 3.8",
79 | ],
80 | )
81 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Contributions are welcome, and they are greatly appreciated\! Every
4 | little bit helps, and credit will always be given.
5 |
6 | You can contribute in many ways:
7 |
8 | ## Types of Contributions
9 |
10 | ### Report Bugs
11 |
12 | Report bugs at
13 | .
14 |
15 | If you are reporting a bug, please include:
16 |
17 | - Your operating system name and version.
18 | - Any details about your local setup that might be helpful in
19 | troubleshooting.
20 | - Detailed steps to reproduce the bug.
21 |
22 | ### Fix Bugs
23 |
24 | Look through the GitHub issues for bugs. Anything tagged with "bug" is
25 | open to whoever wants to implement it.
26 |
27 | ### Implement Features
28 |
29 | Look through the GitHub issues for features. Anything tagged with
30 | "feature" is open to whoever wants to implement it.
31 |
32 | ### Write Documentation
33 |
34 | Django better admin ArrayField could always use more documentation,
35 | whether as part of the official Django better admin ArrayField docs, in
36 | docstrings, or even on the web in blog posts, articles, and such.
37 |
38 | ### Submit Feedback
39 |
40 | The best way to send feedback is to file an issue at
41 | .
42 |
43 | If you are proposing a feature:
44 |
45 | - Explain in detail how it would work.
46 | - Keep the scope as narrow as possible, to make it easier to
47 | implement.
48 | - Remember that this is a volunteer-driven project, and that
49 | contributions are welcome :)
50 |
51 | ## Get Started\!
52 |
53 | Ready to contribute? Here's how to set up
54 | django-better-admin-arrayfield for local
55 | development.
56 |
57 | 1. Fork the
58 | django-better-admin-arrayfield repo
59 | on GitHub.
60 |
61 | 2. Clone your fork
62 | locally:
63 |
64 | $ git clone git@github.com:your_name_here/django-better-admin-arrayfield.git
65 |
66 | 3. Install your local copy into a virtualenv. Assuming you have
67 | virtualenvwrapper installed, this is how you set up your fork for
68 | local development:
69 |
70 | $ mkvirtualenv django-better-admin-arrayfield
71 | $ cd django-better-admin-arrayfield/
72 | $ python setup.py develop
73 |
74 | 4. Create a branch for local development:
75 |
76 | $ git checkout -b name-of-your-bugfix-or-feature
77 |
78 | Now you can make your changes locally.
79 |
80 | 5. When you're done making changes, check that your changes pass flake8
81 | and the tests, including testing other Python versions with tox:
82 |
83 | $ flake8 django_better_admin_arrayfield tests
84 | $ python setup.py test
85 | $ tox
86 |
87 | To get flake8 and tox, just pip install them into your virtualenv.
88 |
89 | 6. Commit your changes and push your branch to GitHub:
90 |
91 | $ git add .
92 | $ git commit -m "Your detailed description of your changes."
93 | $ git push origin name-of-your-bugfix-or-feature
94 |
95 | 7. Submit a pull request through the GitHub website.
96 |
97 | ## Pull Request Guidelines
98 |
99 | Before you submit a pull request, check that it meets these guidelines:
100 |
101 | 1. The pull request should include tests.
102 | 2. If the pull request adds functionality, the docs should be updated.
103 | Put your n
104 |
--------------------------------------------------------------------------------
/sample_project/sample_project/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for sample_project project.
3 |
4 | Generated by 'django-admin startproject' using Django 2.2.5.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.2/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/2.2/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17 |
18 |
19 | # Quick-start development settings - unsuitable for production
20 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
21 |
22 | # SECURITY WARNING: keep the secret key used in production secret!
23 | SECRET_KEY = "af!o7n3efeip8p^%j^%4kde+_&^l51^q%1l3a)yz(223#@j065"
24 |
25 | # SECURITY WARNING: don't run with debug turned on in production!
26 | DEBUG = True
27 |
28 | ALLOWED_HOSTS = []
29 |
30 |
31 | # Application definition
32 |
33 | INSTALLED_APPS = [
34 | "django.contrib.admin",
35 | "django.contrib.auth",
36 | "django.contrib.contenttypes",
37 | "django.contrib.sessions",
38 | "django.contrib.messages",
39 | "django.contrib.staticfiles",
40 | "django_better_admin_arrayfield",
41 | "sample_app.apps.SampleAppConfig",
42 | ]
43 |
44 | MIDDLEWARE = [
45 | "django.middleware.security.SecurityMiddleware",
46 | "django.contrib.sessions.middleware.SessionMiddleware",
47 | "django.middleware.common.CommonMiddleware",
48 | "django.middleware.csrf.CsrfViewMiddleware",
49 | "django.contrib.auth.middleware.AuthenticationMiddleware",
50 | "django.contrib.messages.middleware.MessageMiddleware",
51 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
52 | ]
53 |
54 | ROOT_URLCONF = "sample_project.urls"
55 |
56 | TEMPLATES = [
57 | {
58 | "BACKEND": "django.template.backends.django.DjangoTemplates",
59 | "DIRS": [],
60 | "APP_DIRS": True,
61 | "OPTIONS": {
62 | "context_processors": [
63 | "django.template.context_processors.debug",
64 | "django.template.context_processors.request",
65 | "django.contrib.auth.context_processors.auth",
66 | "django.contrib.messages.context_processors.messages",
67 | ]
68 | },
69 | }
70 | ]
71 |
72 | WSGI_APPLICATION = "sample_project.wsgi.application"
73 |
74 |
75 | # Database
76 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases
77 |
78 | DATABASES = {
79 | "default": {
80 | "ENGINE": "django.db.backends.postgresql_psycopg2",
81 | "NAME": "django_db",
82 | "USER": "sample_user",
83 | "PASSWORD": "12345",
84 | "HOST": "127.0.0.1",
85 | "PORT": "5436",
86 | }
87 | }
88 |
89 | # Password validation
90 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
91 |
92 | AUTH_PASSWORD_VALIDATORS = [
93 | {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
94 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
95 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
96 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
97 | ]
98 |
99 |
100 | # Internationalization
101 | # https://docs.djangoproject.com/en/2.2/topics/i18n/
102 |
103 | LANGUAGE_CODE = "en-us"
104 |
105 | TIME_ZONE = "UTC"
106 |
107 | USE_I18N = True
108 |
109 | USE_L10N = True
110 |
111 | USE_TZ = True
112 |
113 |
114 | # Static files (CSS, JavaScript, Images)
115 | # https://docs.djangoproject.com/en/2.2/howto/static-files/
116 |
117 | STATIC_URL = "/static/"
118 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Django better admin ArrayField
2 |
3 | [](https://badge.fury.io/py/django-better-admin-arrayfield)
4 |
5 | [](https://github.com/gradam/django-better-admin-arrayfield/actions)
6 |
7 | [](https://codecov.io/gh/gradam/django-better-admin-arrayfield)
8 |
9 | Better ArrayField widget for admin
10 |
11 | Supported Python versions: [](https://www.python.org/downloads/release/python-350/) [](https://www.python.org/downloads/release/python-360/) [](https://www.python.org/downloads/release/python-370/) [](https://www.python.org/downloads/release/python-380/)
12 |
13 |
14 |
15 |
16 |
17 | Supported Django versions: 2.0, 2.1, 2.2, 3.0, 3.1
18 |
19 | might work with different django/python versions as well but I did not test that.
20 |
21 | It changes comma separated widget to list based in admin panel.
22 |
23 | Before:
24 | 
25 |
26 | After:
27 | 
28 |
29 | ## Quickstart
30 |
31 | Install Django better admin ArrayField:
32 |
33 | pip install django-better-admin-arrayfield
34 |
35 | Add it to your \`INSTALLED\_APPS\`:
36 |
37 | ```python
38 | INSTALLED_APPS = (
39 | ...
40 | 'django_better_admin_arrayfield',
41 | ...
42 | )
43 | ```
44 |
45 |
46 | ## Usage
47 |
48 | `django_better_admin_arrayfield.models.fields.ArrayField` is a drop-in replacement for standard Django `ArrayField`.
49 |
50 | Import it like below and use it in your model class definition.
51 | ```python
52 | from django_better_admin_arrayfield.models.fields import ArrayField
53 | ```
54 |
55 | Import DynamicArrayMixin like below
56 | ```python
57 | from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
58 | ```
59 |
60 | In your admin class add `DynamicArrayMixin`:
61 | ...
62 | ```python
63 | class MyModelAdmin(admin.ModelAdmin, DynamicArrayMixin):
64 | ```
65 |
66 | That's it.
67 |
68 |
69 | ### Custom subwidget
70 |
71 | By default the subwidget (the one used for each item in the array) will be TextInput. If you want something else, you can use your own specifying it in the `formfield_overrides` of your Admin model:
72 | ```python
73 | class MyWidget(DynamicArrayWidget):
74 | def __init__(self, *args, **kwargs):
75 | kwargs['subwidget_form'] = MyForm
76 | super().__init__(*args, **kwargs)
77 |
78 | class MyModelAdmin(models.ModelAdmin, DynamicArrayMixin):
79 | ...
80 | formfield_overrides = {
81 | DynamicArrayField: {'widget': MyWidget},
82 | }
83 | ```
84 |
85 | If you wanted to have Textarea as the subwidget, you can simply use the included drop-in widget replacement:
86 | ```python
87 | from django_better_admin_arrayfield.forms.widgets import DynamicArrayTextareaWidget
88 |
89 | class MyModelAdmin(models.ModelAdmin, DynamicArrayMixin):
90 | ...
91 | formfield_overrides = {
92 | DynamicArrayField: {'widget': DynamicArrayTextareaWidget},
93 | }
94 | ```
95 |
96 | ## Running Tests
97 |
98 | Does the code actually work?
99 |
100 | source /bin/activate
101 | (myenv) $ pip install tox
102 | (myenv) $ tox
103 |
104 | ## Pre-commit hooks
105 |
106 | Install pre-commit black hook
107 |
108 | source /bin/activate
109 | (myenv) $ pip install -r requirements_dev.txt
110 | (myenv) $ pre-commit install
111 |
112 | ## Credits
113 |
114 | Inspired by: https://stackoverflow.com/a/49370480/4638248
115 |
116 | Tools used in rendering this
117 | package:
118 |
119 | - [Cookiecutter](https://github.com/audreyr/cookiecutter)
120 | - [cookiecutter-djangopackage](https://github.com/pydanny/cookiecutter-djangopackage)
121 |
--------------------------------------------------------------------------------