feat(people): Add first user data fields

To support choices in CharFields, Django choices enums have been
backported by bringing in the source file from Django 3.0.3

See #1
This commit is contained in:
James Graham
2020-02-21 17:10:26 +00:00
parent e4a50dbfa4
commit 6d2737b1a6
11 changed files with 194 additions and 5 deletions

6
backports/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
"""
These files are backported functionality from future versions of Django.
- models.db.enums - copied from Django 3.0.3
See https://github.com/django/django/blob/3.0.3/django/db/models/enums.py
"""

0
backports/db/__init__.py Normal file
View File

View File

View File

@@ -0,0 +1,82 @@
import enum
from django.utils.functional import Promise
__all__ = ['Choices', 'IntegerChoices', 'TextChoices']
class ChoicesMeta(enum.EnumMeta):
"""A metaclass for creating a enum choices."""
def __new__(metacls, classname, bases, classdict):
labels = []
for key in classdict._member_names:
value = classdict[key]
if (
isinstance(value, (list, tuple)) and
len(value) > 1 and
isinstance(value[-1], (Promise, str))
):
*value, label = value
value = tuple(value)
else:
label = key.replace('_', ' ').title()
labels.append(label)
# Use dict.__setitem__() to suppress defenses against double
# assignment in enum's classdict.
dict.__setitem__(classdict, key, value)
cls = super().__new__(metacls, classname, bases, classdict)
cls._value2label_map_ = dict(zip(cls._value2member_map_, labels))
# Add a label property to instances of enum which uses the enum member
# that is passed in as "self" as the value to use when looking up the
# label in the choices.
cls.label = property(lambda self: cls._value2label_map_.get(self.value))
cls.do_not_call_in_templates = True
return enum.unique(cls)
def __contains__(cls, member):
if not isinstance(member, enum.Enum):
# Allow non-enums to match against member values.
return member in {x.value for x in cls}
return super().__contains__(member)
@property
def names(cls):
empty = ['__empty__'] if hasattr(cls, '__empty__') else []
return empty + [member.name for member in cls]
@property
def choices(cls):
empty = [(None, cls.__empty__)] if hasattr(cls, '__empty__') else []
return empty + [(member.value, member.label) for member in cls]
@property
def labels(cls):
return [label for _, label in cls.choices]
@property
def values(cls):
return [value for value, _ in cls.choices]
class Choices(enum.Enum, metaclass=ChoicesMeta):
"""Class for creating enumerated choices."""
def __str__(self):
"""
Use value when cast to str, so that Choices set as model instance
attributes are rendered as expected in templates and similar contexts.
"""
return str(self.value)
class IntegerChoices(int, Choices):
"""Class for creating enumerated integer choices."""
pass
class TextChoices(str, Choices):
"""Class for creating enumerated string choices."""
def _generate_next_value_(name, start, count, last_values):
return name

View File

@@ -12,9 +12,9 @@ class PersonForm(forms.ModelForm):
""" """
class Meta: class Meta:
model = models.Person model = models.Person
fields = [ exclude = [
'name', 'user',
'core_member', 'relationship_targets',
] ]

View File

@@ -0,0 +1,23 @@
# Generated by Django 2.2.10 on 2020-02-21 16:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('people', '0008_order_answer_by_question'),
]
operations = [
migrations.AddField(
model_name='person',
name='age_group',
field=models.CharField(blank=True, choices=[('<=25', '25 or under'), ('26-30', '26-30'), ('31-35', '31-35'), ('36-40', '36-40'), ('41-45', '41-45'), ('46-50', '46-50'), ('51-55', '51-55'), ('56-60', '56-60'), ('>=61', '61 or older'), ('N', 'Prefer not to say')], max_length=5),
),
migrations.AddField(
model_name='person',
name='gender',
field=models.CharField(blank=True, choices=[('M', 'Male'), ('F', 'Female'), ('O', 'Other'), ('N', 'Prefer not to say')], max_length=1),
),
]

View File

@@ -4,6 +4,9 @@ from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from backports.db.models.enums import TextChoices
class User(AbstractUser): class User(AbstractUser):
@@ -39,6 +42,35 @@ class Person(models.Model):
through_fields=('source', 'target'), through_fields=('source', 'target'),
symmetrical=False) symmetrical=False)
###############################################################
# Data collected for analysis of community makeup and structure
class GenderChoices(TextChoices):
MALE = 'M', _('Male')
FEMALE = 'F', _('Female')
OTHER = 'O', _('Other')
PREFER_NOT_TO_SAY = 'N', _('Prefer not to say')
gender = models.CharField(max_length=1,
choices=GenderChoices.choices,
blank=True, null=False)
class AgeGroupChoices(TextChoices):
LTE_25 = '<=25', _('25 or under')
BETWEEN_26_30 = '26-30', _('26-30')
BETWEEN_31_35 = '31-35', _('31-35')
BETWEEN_36_40 = '36-40', _('36-40')
BETWEEN_41_45 = '41-45', _('41-45')
BETWEEN_46_50 = '46-50', _('46-50')
BETWEEN_51_55 = '51-55', _('51-55')
BETWEEN_56_60 = '56-60', _('56-60')
GTE_61 = '>=61', _('61 or older')
PREFER_NOT_TO_SAY = 'N', _('Prefer not to say')
age_group = models.CharField(max_length=5,
choices=AgeGroupChoices.choices,
blank=True, null=False)
@property @property
def relationships(self): def relationships(self):
return self.relationships_as_source.all().union( return self.relationships_as_source.all().union(

View File

@@ -10,6 +10,9 @@
</ol> </ol>
</nav> </nav>
<a class="btn btn-success"
href="{% url 'people:person.update' pk=person.pk %}">Update</a>
<hr> <hr>
<h2>Relationships As Source</h2> <h2>Relationships As Source</h2>

View File

@@ -0,0 +1,30 @@
{% extends 'base.html' %}
{% block content %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{% url 'people:person.list' %}">People</a>
</li>
<li class="breadcrumb-item">
<a href="{% url 'people:person.detail' pk=person.pk %}">{{ person }}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">Update</li>
</ol>
</nav>
<hr>
<form class="form"
method="POST">
{% csrf_token %}
{% load bootstrap4 %}
{% bootstrap_form form %}
{% buttons %}
<button class="btn btn-success" type="submit">Submit</button>
{% endbuttons %}
</form>
{% endblock %}

View File

@@ -22,6 +22,10 @@ urlpatterns = [
views.ProfileView.as_view(), views.ProfileView.as_view(),
name='person.detail'), name='person.detail'),
path('people/<int:pk>/update',
views.PersonUpdateView.as_view(),
name='person.update'),
path('people/<int:person_pk>/relationships/create', path('people/<int:person_pk>/relationships/create',
views.RelationshipCreateView.as_view(), views.RelationshipCreateView.as_view(),
name='person.relationship.create'), name='person.relationship.create'),

View File

@@ -3,7 +3,7 @@ Views for displaying or manipulating models in the 'people' app.
""" """
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.views.generic import CreateView, DetailView, ListView from django.views.generic import CreateView, DetailView, ListView, UpdateView
from . import forms, models from . import forms, models
@@ -53,6 +53,15 @@ class ProfileView(DetailView):
return self.request.user.person return self.request.user.person
class PersonUpdateView(UpdateView):
"""
View for updating a :class:`Person` record.
"""
model = models.Person
template_name = 'people/person/update.html'
form_class = forms.PersonForm
class RelationshipDetailView(DetailView): class RelationshipDetailView(DetailView):
""" """
View displaying details of a :class:`Relationship`. View displaying details of a :class:`Relationship`.