.*)$" % settings.MEDIA_URL[1:],
23 | serve,
24 | {"document_root": settings.MEDIA_ROOT, "show_indexes": True},
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/example/example/wsgi.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.core.wsgi import get_wsgi_application
4 |
5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
6 | application = get_wsgi_application()
7 |
--------------------------------------------------------------------------------
/example/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | import sys
5 |
6 | from django.conf import ENVIRONMENT_VARIABLE
7 |
8 | if __name__ == "__main__":
9 | os.environ.setdefault(ENVIRONMENT_VARIABLE, "example.settings")
10 | from django.core.management import execute_from_command_line
11 |
12 | execute_from_command_line(sys.argv)
13 |
--------------------------------------------------------------------------------
/radiogrid/__init__.py:
--------------------------------------------------------------------------------
1 | from .db import RadioGridField
2 | from .fields import RadioGridFormField
3 | from .widgets import RadioGridWidget
4 |
5 | __version__ = "1.1.0"
6 |
--------------------------------------------------------------------------------
/radiogrid/compat.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from django import VERSION
4 |
5 |
6 | def get_val_from_obj(self, obj):
7 | if VERSION < (2, 0):
8 | return self._get_val_from_obj(obj)
9 | if obj is not None:
10 | return getattr(obj, self.attname)
11 | else:
12 | return self.get_default()
13 |
--------------------------------------------------------------------------------
/radiogrid/db.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from django.core.exceptions import ValidationError
4 | from django.db.models import TextField
5 |
6 | from .compat import get_val_from_obj
7 | from .fields import RadioGridFormField
8 |
9 |
10 | class RadioGridField(TextField):
11 | def __init__(self, *args, **kwargs):
12 | self.rows = kwargs.pop("rows")
13 | self.values = kwargs.pop("values")
14 | self.require_all_fields = kwargs.pop("require_all_fields", True)
15 | self.verbose_name = kwargs.get("verbose_name", "")
16 | super(RadioGridField, self).__init__(*args, **kwargs)
17 |
18 | def deconstruct(self):
19 | name, path, args, kwargs = super(RadioGridField, self).deconstruct()
20 | kwargs["rows"] = self.rows
21 | kwargs["values"] = self.values
22 | kwargs["require_all_fields"] = self.require_all_fields
23 | return name, path, args, kwargs
24 |
25 | def from_db_value(self, value, *args, **kwargs):
26 | return self.to_python(value)
27 |
28 | def to_python(self, value):
29 | return value if isinstance(value, list) else value.split(",")
30 |
31 | def get_prep_value(self, value):
32 | return "" if value is None else ",".join(value)
33 |
34 | def formfield(self, **kwargs):
35 | defaults = {
36 | "rows": self.rows,
37 | "values": self.values,
38 | "require_all_fields": self.require_all_fields,
39 | "label": self.verbose_name,
40 | "required": self.require_all_fields,
41 | }
42 | defaults.update(kwargs)
43 | return RadioGridFormField(**defaults)
44 |
45 | def value_to_string(self, obj):
46 | return self.get_prep_value(get_val_from_obj(self, obj))
47 |
48 | def validate(self, value, model_instance):
49 | allowed_values = [str(key) for key, _ in self.values]
50 | if not self.require_all_fields:
51 | allowed_values += [""]
52 |
53 | for v in value:
54 | if str(v) not in allowed_values:
55 | raise ValidationError(
56 | self.error_messages["invalid_choice"] % {"value": value}
57 | )
58 |
59 | def contribute_to_class(self, cls, name, **kwargs):
60 | super(RadioGridField, self).contribute_to_class(cls, name, **kwargs)
61 |
62 | def get_list(obj):
63 | values = dict(self.values)
64 | display = []
65 | for value in getattr(obj, name):
66 | item = values.get(value, None)
67 | if item is None:
68 | try:
69 | item = values.get(int(value), value)
70 | except (ValueError, TypeError):
71 | item = value
72 | display.append(item)
73 | return display
74 |
75 | def get_display(obj):
76 | return ", ".join(get_list(obj))
77 |
78 | setattr(cls, "get_%s_list" % self.name, get_list)
79 | setattr(cls, "get_%s_display" % self.name, get_display)
80 |
--------------------------------------------------------------------------------
/radiogrid/fields.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from django.forms import ChoiceField, MultiValueField
4 |
5 | from .widgets import RadioGridWidget
6 |
7 |
8 | class RadioGridFormField(MultiValueField):
9 | def __init__(self, rows, values, *args, **kwargs):
10 | kwargs["widget"] = RadioGridWidget(rows, values)
11 | kwargs["fields"] = [ChoiceField(choices=values, required=False) for _ in rows]
12 | super(RadioGridFormField, self).__init__(*args, **kwargs)
13 |
14 | def compress(self, data_list):
15 | return "" if data_list is None else ",".join(data_list)
16 |
--------------------------------------------------------------------------------
/radiogrid/templates/radiogrid/radiogrid_input.html:
--------------------------------------------------------------------------------
1 | {% with id=widget.attrs.id %}
2 | {% for _, options, _ in tr.optgroups %}
3 | {% for option in options %}
4 | {% include option.template_name with widget=option %} |
5 | {% endfor %}
6 | {% endfor %}
7 | {% endwith %}
8 |
--------------------------------------------------------------------------------
/radiogrid/templates/radiogrid/radiogrid_widget.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | |
5 | {% for value, name in values %}
6 | {{ name }} |
7 | {% endfor %}
8 |
9 |
10 |
11 | {% for name, tr in rows %}
12 |
13 |
14 | {{ name }}
15 | |
16 | {% if tr.template_name %}
17 | {% include tr.template_name %}
18 | {% else %}
19 | {{ tr }}
20 | {% endif %}
21 |
22 | {% endfor %}
23 |
24 |
25 |
--------------------------------------------------------------------------------
/radiogrid/widgets.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from django.forms import MultiWidget, RadioSelect
4 |
5 |
6 | class RadioRadioSelect(RadioSelect):
7 | template_name = "radiogrid/radiogrid_input.html"
8 |
9 |
10 | class RadioGridWidget(MultiWidget):
11 | input_type = "grid"
12 | template_name = "radiogrid/radiogrid_widget.html"
13 |
14 | def __init__(self, rows, values, attrs=None):
15 | self.rows = rows
16 | self.values = values
17 | choices = [(k, "") for k, _ in values]
18 | widgets = [RadioRadioSelect(choices=choices, attrs=attrs) for _ in rows]
19 | super(RadioGridWidget, self).__init__(widgets, attrs)
20 |
21 | def get_context(self, name, value, attrs):
22 | context = super(RadioGridWidget, self).get_context(name, value, attrs)
23 | widgets = context["widget"]["subwidgets"]
24 | return {
25 | "rows": [(v[1], widgets[i]) for i, v in enumerate(self.rows)],
26 | "values": self.values,
27 | "attrs": self.attrs,
28 | }
29 |
30 | def decompress(self, value):
31 | if value:
32 | return value.split(",")
33 | return [None for _ in self.rows]
34 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sinkler/django-radiogrid/a20274adb04028fa7acb6c5f671b1a4c48a8eb4b/screenshot.png
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal=1
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | import re
5 | import sys
6 |
7 | from setuptools import setup
8 |
9 | if sys.version_info[0] < 3:
10 | from setuptools import find_packages as find_namespace_packages
11 | else:
12 | from setuptools import find_namespace_packages
13 |
14 |
15 | def read(filename):
16 | return open(os.path.join(os.path.dirname(__file__), filename)).read()
17 |
18 |
19 | def get_version(package):
20 | init_py = open(os.path.join(package, "__init__.py")).read()
21 | return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
22 |
23 |
24 | setup(
25 | name="django-radiogrid",
26 | version=get_version("radiogrid"),
27 | author="Anton Shurashov",
28 | author_email="sinkler@sinkler.ru",
29 | description="Django radio grid field",
30 | long_description=(read("README.rst") + "\n\n" + read("CHANGES.rst")),
31 | long_description_content_type="text/x-rst",
32 | install_requires=["django>=1.7.0", "six>=1.16.0"],
33 | classifiers=[
34 | "Development Status :: 5 - Production/Stable",
35 | "Framework :: Django",
36 | "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
37 | "Programming Language :: Python :: 2",
38 | "Programming Language :: Python :: 3",
39 | ],
40 | license="LGPL 3",
41 | keywords="django,radio,grid,field,choices",
42 | url="https://github.com/Sinkler/django-radiogrid",
43 | packages=find_namespace_packages(),
44 | include_package_data=True,
45 | zip_safe=False,
46 | )
47 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py27-django{111}, py{37,38,39,310}-django{111,22,32,40,master}
3 |
4 | [gh-actions]
5 | python =
6 | 2.7: py27
7 | 3.7: py37
8 | 3.8: py38
9 | 3.9: py39
10 | 3.10: py310
11 |
12 | [testenv]
13 | usedevelop = True
14 | commands =
15 | {envbindir}/coverage run -p example/manage.py test example.app
16 | coverage combine
17 | coverage report
18 | coverage xml -o ./coverage.xml
19 | deps =
20 | coverage
21 | django-111: Django>=1.11rc1,<2.0
22 | django-22: Django>=2.2,<3.0
23 | django-32: Django>=3.2,<4.0
24 | django-40: Django>=4.0,<4.1
25 | django-master: https://github.com/django/django/archive/master.tar.gz
26 |
--------------------------------------------------------------------------------