diff --git a/accounts/forms.py b/accounts/forms.py index 461d3ae0b..6982951e3 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -365,6 +365,9 @@ class Meta: "us_race_ethnicity_identification_describe": forms.Textarea( attrs={"rows": 2} ), + "us_race_ethnicity_identification": BitFieldCheckboxSelectMultiple( + attrs={"class": "column-checkbox"} + ), } diff --git a/accounts/migrations/0055_replace_multiselectfield.py b/accounts/migrations/0055_replace_multiselectfield.py new file mode 100644 index 000000000..a691e92f1 --- /dev/null +++ b/accounts/migrations/0055_replace_multiselectfield.py @@ -0,0 +1,47 @@ +# Generated by Django 3.2.11 on 2023-01-25 16:20 + +import operator +from functools import reduce + +import bitfield.models +from django.db import migrations + +FIELDS = ( + ("white", "White"), + ("hisp", "Hispanic, Latino, or Spanish origin"), + ("black", "Black or African American"), + ("asian", "Asian"), + ("native", "American Indian or Alaska Native"), + ("mideast-naf", "Middle Eastern or North African"), + ("hawaiian-pac-isl", "Native Hawaiian or Other Pacific Islander"), + ("other", "Another race, ethnicity, or origin"), +) + + +def encode_ethnicity(apps, schema_editor): + fields = [v[0] for v in FIELDS] + for demo_data in apps.get_model("accounts", "DemographicData").objects.all(): + masks = ( + 2 ** fields.index(v) + for v in demo_data.us_race_ethnicity_identification + if v in fields + ) + encoded_value = reduce(operator.__or__, masks, 0) + demo_data.us_race_ethnicity_identification = [encoded_value] + demo_data.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0054_update_demo_fields"), + ] + + operations = [ + migrations.RunPython(encode_ethnicity), + migrations.AlterField( + model_name="demographicdata", + name="us_race_ethnicity_identification", + field=bitfield.models.BitField(FIELDS, default=0), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 448e75cb3..e34717797 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -25,7 +25,6 @@ from localflavor.us.models import USStateField from localflavor.us.us_states import USPS_CHOICES from model_utils import Choices -from multiselectfield import MultiSelectField from qrcode import make as make_qrcode from qrcode.image.svg import SvgPathImage @@ -395,7 +394,7 @@ class JSONAPIMeta: class DemographicData(models.Model): - RACE_CHOICES = Choices( + RACE_CHOICES = ( ("white", _("White")), ("hisp", _("Hispanic, Latino, or Spanish origin")), ("black", _("Black or African American")), @@ -519,9 +518,7 @@ class DemographicData(models.Model): choices=GUARDIAN_CHOICES, max_length=6, blank=True ) guardians_explanation = models.TextField(blank=True) - us_race_ethnicity_identification = MultiSelectField( - choices=RACE_CHOICES, blank=True - ) + us_race_ethnicity_identification = BitField(flags=RACE_CHOICES, default=0) us_race_ethnicity_identification_describe = models.TextField(blank=True) age = models.CharField(max_length=5, choices=AGE_CHOICES, blank=True) gender = models.CharField(max_length=2, choices=GENDER_CHOICES, blank=True) diff --git a/poetry.lock b/poetry.lock index 8bd56e95e..bc7459fa3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. [[package]] name = "amqp" @@ -461,6 +461,18 @@ editorconfig = ">=0.12.2" jsbeautifier = "*" six = ">=1.13.0" +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + [[package]] name = "distlib" version = "0.3.4" @@ -510,20 +522,36 @@ url = "https://github.com/lookit/django-ace-overlay.git" reference = "master" resolved_reference = "4b174f78c6d6aaaee961e220c9461cbe74c861da" +[[package]] +name = "django-allauth" +version = "0.42.0" +description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "django-allauth-0.42.0.tar.gz", hash = "sha256:f17209410b7f87da0a84639fd79d3771b596a6d3fc1a8e48ce50dabc7f441d30"}, +] + +[package.dependencies] +Django = ">=2.0" +python3-openid = ">=3.0.8" +requests = "*" +requests-oauthlib = ">=0.3.0" + [[package]] name = "django-bitfield" -version = "2.1.0" +version = "2.2.0" description = "BitField in Django" category = "main" optional = false python-versions = "*" files = [ - {file = "django-bitfield-2.1.0.tar.gz", hash = "sha256:a55859fd16ce4269d5ceed3e20cf8fc3c2df866f0a78b90c60a19a0e76aa5fd8"}, - {file = "django_bitfield-2.1.0-py2.py3-none-any.whl", hash = "sha256:158f1056e22cce450d0a49633ea77bfd84b85a2294b1ef03faa775a485f4065d"}, + {file = "django-bitfield-2.2.0.tar.gz", hash = "sha256:1b21262acc4ec0af3f82ed04498a056cd9d5452532ac02771e004835a34e0b1b"}, ] [package.dependencies] -Django = ">=1.10.8" +Django = ">=1.11.29" six = "*" [package.extras] @@ -701,21 +729,6 @@ files = [ [package.dependencies] Django = ">=2.0.1" -[[package]] -name = "django-multiselectfield" -version = "0.1.12" -description = "Django multiple select field" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "django-multiselectfield-0.1.12.tar.gz", hash = "sha256:d0a4c71568fb2332c71478ffac9f8708e01314a35cf923dfd7a191343452f9f9"}, - {file = "django_multiselectfield-0.1.12-py3-none-any.whl", hash = "sha256:c270faa7f80588214c55f2d68cbddb2add525c2aa830c216b8a198de914eb470"}, -] - -[package.dependencies] -django = ">=1.4" - [[package]] name = "django-pandas" version = "0.6.2" @@ -1499,6 +1512,23 @@ files = [ {file = "numpy-1.22.3.zip", hash = "sha256:dbc7601a3b7472d559dc7b933b18b4b66f9aa7452c120e87dfb33d02008c8a18"}, ] +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + [[package]] name = "pandas" version = "1.4.1" @@ -2019,6 +2049,25 @@ soap = ["zeep"] soap-alt = ["suds"] soap-fallback = ["PySimpleSOAP"] +[[package]] +name = "python3-openid" +version = "3.2.0" +description = "OpenID support for modern servers and consumers." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "python3-openid-3.2.0.tar.gz", hash = "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf"}, + {file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"}, +] + +[package.dependencies] +defusedxml = "*" + +[package.extras] +mysql = ["mysql-connector-python"] +postgresql = ["psycopg2"] + [[package]] name = "pytz" version = "2022.1" @@ -2245,6 +2294,25 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"] +[[package]] +name = "requests-oauthlib" +version = "1.3.1" +description = "OAuthlib authentication support for Requests." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, + {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + [[package]] name = "rsa" version = "4.8" @@ -2664,4 +2732,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = ">=3.9 <3.10" -content-hash = "ec3aa9f62f5d4fc855ea5bead7153e3e95f02cdf2e2719ad2b7b851528d80861" +content-hash = "6eb5409e070544819e8c58e3cd839b018fa51cc985f35115f47a34bc3b7c12a6" diff --git a/pyproject.toml b/pyproject.toml index 124065312..586a48b96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,8 +11,9 @@ celery = "4.4.7" ciso8601 = "2.1.3" Django = "3.2.11" django-ace-overlay = { git = "https://github.com/lookit/django-ace-overlay.git", branch = "master" } -django-bitfield = "2.1.0" django-bootstrap5 = "22.2" +django-allauth = "0.42.0" +django-bitfield = "2.2.0" django-celery-beat = "2.0.0" django-cors-headers = "3.13.0" django-countries = "7.2.1" @@ -21,7 +22,6 @@ django-filter = "2.4.0" django-guardian = "2.3.0" django-localflavor = "3.1" django-model-utils = "4.0.0" -django-multiselectfield = "0.1.12" django-pandas = "0.6.2" django-prettyjson = "0.4.1" django-revproxy = { git = "https://github.com/Innovativity/django-revproxy.git", branch = "b9fa8375d03fd68747dcb7273a97c19d788aa51b" } diff --git a/web/tests/test_views.py b/web/tests/test_views.py index 825a8abe7..efeb49c2e 100644 --- a/web/tests/test_views.py +++ b/web/tests/test_views.py @@ -228,10 +228,15 @@ def test_demographic_data_update_authenticated(self): # Update data and save data["country"] = "BR" + + # Setting ethnicity to None fixes issue with submitting data. + data["us_race_ethnicity_identification"] = None + cleaned_data = {key: val for (key, val) in data.items() if val is not None} response = self.client.post( reverse("web:demographic-data-update"), cleaned_data, follow=True ) + self.assertEqual(response.context["form"].errors, {}) self.assertEqual( response.redirect_chain, [(reverse("web:demographic-data-update"), 302)] )