Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add family attendees datagrid #3

Merged
merged 20 commits into from
May 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1ab6ee6
prepare for family UI
xjlin0 May 15, 2021
5a523ec
prepare for family attendee datagrid
xjlin0 May 15, 2021
f4d7867
remove family attendee label to create more space for the future data…
xjlin0 May 15, 2021
5cff421
first Datagrid of Family attendee works, but should I use server side…
xjlin0 May 16, 2021
8d8da6a
switch to browser side processing for family attendee datagrid
xjlin0 May 16, 2021
7525724
added some columns and it seems family attendee datagrid need to move…
xjlin0 May 16, 2021
fd0d6fe
moved family attendee datagrid down for wider block
xjlin0 May 16, 2021
1af1afb
add all relation endpoint
xjlin0 May 16, 2021
6083966
make family grouped in the family & relation block
xjlin0 May 16, 2021
89d9aed
skip the FamilyAttendee order by family created time temporarily
xjlin0 May 16, 2021
e9050c8
preparing for cell editing of family attendee datagrid
xjlin0 May 16, 2021
3efb56b
enabling Family Attendee Datagrid Cell editing mode successfully
xjlin0 May 16, 2021
3b7505c
change jQuery syntax to object getting
xjlin0 May 16, 2021
693ec4c
now all family attendee are shown as a link, and column switching upo…
xjlin0 May 16, 2021
88a5ea0
move contact to upper block so future move of contacts to Basic Info …
xjlin0 May 16, 2021
1b29c5e
change Family attendee datagrid's datasource to a CustomStore for fut…
xjlin0 May 17, 2021
3429d05
Devextreme Datagrid cell editing only send partial data to server, we…
xjlin0 May 17, 2021
226443c
change endpoint /persons/api/datagrid_data_familyattendees/ to check …
xjlin0 May 17, 2021
b530eba
cell editing on FamilyAttendee Datagrid works
xjlin0 May 17, 2021
ddff85b
now creating attendee in FamilyAttendee DxDatagrid works
xjlin0 May 18, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,6 @@ https://dbdiagram.io/d/5d5ff66eced98361d6dddc48
* Enter Django console by `docker-compose -f local.yml run django python manage.py shell_plus`
* remote debug in PyCharm for docker, please check [django cookie doc](https://github.com/pydanny/cookiecutter-django/blob/master/{{cookiecutter.project_slug}}/docs/pycharm/configuration.rst).

## Todo:

* Modify Attendee save method to combine/convert names by OpenCC to support searches in different text encoding, and possibly retire db level full_name.
2 changes: 1 addition & 1 deletion attendees/persons/migrations/0012_family_attendee_m2m.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Migration(migrations.Migration):
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('is_removed', models.BooleanField(default=False)),
('display_order', models.SmallIntegerField(db_index=True, default=1, help_text="0 will be household header")),
('display_order', models.SmallIntegerField(db_index=True, default=1, help_text="0 will be first family")),
('attendee', models.ForeignKey(on_delete=models.CASCADE, to='persons.Attendee')),
('family', models.ForeignKey(on_delete=models.CASCADE, to='persons.Family')),
('role', models.ForeignKey(help_text='[Title] the family role of the attendee?', on_delete=models.SET(0), related_name='role', to='persons.Relation', verbose_name='attendee is')),
Expand Down
2 changes: 1 addition & 1 deletion attendees/persons/models/attendee.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def division_label(self):

@cached_property
def family_members(self):
return self.__class__.objects.filter(families__in=self.families.all())
return self.__class__.objects.filter(families__in=self.families.all()).distinct()

@cached_property
def self_phone_numbers(self):
Expand Down
2 changes: 1 addition & 1 deletion attendees/persons/models/family_attendee.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class FamilyAttendee(TimeStampedModel, SoftDeletableModel):
family = models.ForeignKey('persons.Family', null=False, blank=False, on_delete=models.CASCADE)
attendee = models.ForeignKey('persons.Attendee', null=False, blank=False, on_delete=models.CASCADE)
role = models.ForeignKey('persons.Relation', related_name='role', null=False, blank=False, on_delete=models.SET(0), verbose_name='attendee is', help_text="[Title] the family role of the attendee?")
display_order = models.SmallIntegerField(default=1, blank=False, null=False, db_index=True, help_text="0 will be household header")
display_order = models.SmallIntegerField(default=1, blank=False, null=False, db_index=True, help_text="0 will be first family") # In current Attendee update page, FamilyAttendee order by created of family
start = models.DateField(null=True, blank=True, help_text='date joining family')
finish = models.DateField(null=True, blank=True, help_text='date leaving family')

Expand Down
4 changes: 3 additions & 1 deletion attendees/persons/serializers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# order matters
from .relation_serializer import RelationSerializer
from .attendee import AttendeeSerializer
from .attending import AttendingSerializer
from .family_serializer import FamilySerializer
from .family_attendee_serializer import FamilyAttendeeSerializer
from .attendee_minimal_serializer import AttendeeMinimalSerializer
from .attending_attendee_serializer import AttendingAttendeeSerializer
from .attendingmeet_etc_serializer import AttendingMeetEtcSerializer
from .attendee import AttendeeSerializer

10 changes: 5 additions & 5 deletions attendees/persons/serializers/attendee.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@


class AttendeeSerializer(serializers.ModelSerializer):
parents_notifiers_names = serializers.CharField()
self_email_addresses = serializers.CharField()
caregiver_email_addresses = serializers.CharField()
self_phone_numbers = serializers.CharField()
caregiver_phone_numbers = serializers.CharField()
parents_notifiers_names = serializers.CharField(read_only=True)
self_email_addresses = serializers.CharField(read_only=True)
caregiver_email_addresses = serializers.CharField(read_only=True)
self_phone_numbers = serializers.CharField(read_only=True)
caregiver_phone_numbers = serializers.CharField(read_only=True)

class Meta:
model = Attendee
Expand Down
68 changes: 61 additions & 7 deletions attendees/persons/serializers/family_attendee_serializer.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,69 @@
from rest_framework import serializers

from attendees.persons.models import FamilyAttendee
from attendees.persons.serializers import FamilySerializer
from attendees.persons.models import FamilyAttendee, Family, Attendee
from attendees.persons.serializers import FamilySerializer, AttendeeSerializer


class FamilyAttendeeSerializer(serializers.ModelSerializer):
family = FamilySerializer(read_only=True)
family = FamilySerializer(many=False)
attendee = AttendeeSerializer(many=False)

class Meta:
model = FamilyAttendee
# fields = '__all__'
fields = [f.name for f in model._meta.fields if f.name not in ['is_removed']] + [
'family',
]
fields = '__all__'
# fields = [f.name for f in model._meta.fields if f.name not in ['is_removed']] + [
# 'family',
# ]

def create(self, validated_data):
"""
Create or update `FamilyAttendee` instance, given the validated data.
"""
familyattendee_id = self._kwargs.get('data', {}).get('id')
new_family = Family.objects.filter(pk=self._kwargs.get('data', {}).get('family', {}).get('id')).first()
new_attendee_data = validated_data.get('attendee', {})
if new_family:
validated_data['family'] = new_family

if new_attendee_data:
attendee, attendee_created = Attendee.objects.update_or_create(
id=new_attendee_data.get('id'),
defaults=new_attendee_data,
)
validated_data['attendee'] = attendee

obj, created = FamilyAttendee.objects.update_or_create(
id=familyattendee_id,
defaults=validated_data,
)
return obj

def update(self, instance, validated_data):
"""
Update and return an existing `FamilyAttendee` instance, given the validated data.

"""
new_family = Family.objects.filter(pk=self._kwargs.get('data', {}).get('family', {}).get('id')).first()
new_attendee_data = validated_data.get('attendee', {})

if new_family:
# instance.family = new_family
validated_data['family'] = new_family
# else:
# validated_data['family'] = instance.family

if new_attendee_data:
attendee, attendee_created = Attendee.objects.update_or_create(
id=instance.attendee.id,
defaults=new_attendee_data,
)
validated_data['attendee'] = attendee
# else:
# validated_data['attendee'] = instance.attendee

obj, created = FamilyAttendee.objects.update_or_create(
id=instance.id,
defaults=validated_data,
)

return obj
10 changes: 10 additions & 0 deletions attendees/persons/serializers/relation_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from attendees.persons.models import Relation
from rest_framework import serializers


class RelationSerializer(serializers.ModelSerializer):

class Meta:
model = Relation
fields = '__all__'

24 changes: 18 additions & 6 deletions attendees/persons/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
from rest_framework import routers

from attendees.persons.views import (
api_all_relations_viewset,
api_assembly_meet_attendings_viewset,
api_attendee_families_viewset,
api_data_attendings_viewset,
api_datagrid_data_attendees_viewset,
api_datagrid_data_attendee_viewset,
api_datagrid_data_attendingmeet_viewset,
api_assembly_meet_attendees_viewset,
api_datagrid_data_familyattendees_viewset,
datagrid_assembly_all_attendings_list_view,
datagrid_assembly_data_attendees_list_view,
datagrid_assembly_data_attendings_list_view,
Expand Down Expand Up @@ -69,12 +72,21 @@
api_datagrid_data_attendingmeet_viewset,
basename='attendingmeet',
)
# router.register(
# 'api/datagrid_data_place/(?P<place_id>.+)',
# api_datagrid_data_place_viewset,
# basename='place',
# )

router.register(
'api/datagrid_data_familyattendees',
api_datagrid_data_familyattendees_viewset,
basename='familyattendee',
)
router.register(
'api/all_relations',
api_all_relations_viewset,
basename='relation',
)
router.register(
'api/attendee_families/(?P<attendee_id>.+)',
api_attendee_families_viewset,
basename='family',
)

urlpatterns = [
path('',
Expand Down
3 changes: 3 additions & 0 deletions attendees/persons/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from .api.all_relations import api_all_relations_viewset
from .api.assembly_meet_attendings import api_assembly_meet_attendings_viewset
from .api.data_attendings import api_data_attendings_viewset
from .api.attendee_families import api_attendee_families_viewset
from .api.attendee_attendings import api_attendee_attendings_viewset
from .api.datagrid_data_attendees import api_datagrid_data_attendees_viewset
from .api.datagrid_data_attendingmeet import api_datagrid_data_attendingmeet_viewset
from .api.datagrid_data_attendee import api_datagrid_data_attendee_viewset
from .api.datagrid_data_familyattendees import api_datagrid_data_familyattendees_viewset
from .api.assembly_meet_attendees import api_assembly_meet_attendees_viewset
from .page.datagrid_assembly_all_attendings import datagrid_assembly_all_attendings_list_view
from .page.datagrid_assembly_data_attendees import datagrid_assembly_data_attendees_list_view
Expand Down
23 changes: 23 additions & 0 deletions attendees/persons/views/api/all_relations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.contrib.auth.mixins import LoginRequiredMixin

from rest_framework import viewsets

from attendees.persons.models import Relation
from attendees.persons.serializers import RelationSerializer


class ApiAllRelationsViewsSet(LoginRequiredMixin, viewsets.ModelViewSet):
"""
API endpoint that allows Relation(Role) to be viewed or edited.
"""
serializer_class = RelationSerializer

def get_queryset(self):
relation_id = self.request.query_params.get('relation_id')
if relation_id:
return Relation.objects.filter(pk=relation_id)
else:
return Relation.objects.order_by('display_order')


api_all_relations_viewset = ApiAllRelationsViewsSet
26 changes: 26 additions & 0 deletions attendees/persons/views/api/attendee_families.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404

from rest_framework import viewsets

from attendees.persons.models import Attendee
from attendees.persons.serializers import FamilySerializer
from attendees.users.authorization.route_guard import SpyGuard


class ApiAttendeeFamiliesViewsSet(LoginRequiredMixin, SpyGuard, viewsets.ModelViewSet):
"""
API endpoint that allows families(attendees) of an Attendee to be viewed or edited.
"""
serializer_class = FamilySerializer

def get_queryset(self):
attendee = get_object_or_404(Attendee, pk=self.kwargs.get('attendee_id'))
family_id = self.request.query_params.get('family_id')
if family_id:
return attendee.families.filter(pk=family_id)
else:
return attendee.families.order_by('display_order')


api_attendee_families_viewset = ApiAttendeeFamiliesViewsSet
66 changes: 66 additions & 0 deletions attendees/persons/views/api/datagrid_data_familyattendees.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from rest_framework import viewsets

from attendees.persons.models import Attendee, FamilyAttendee
from attendees.persons.serializers import FamilyAttendeeSerializer
from attendees.users.authorization.route_guard import SpyGuard


class ApiDatagridDataFamilyAttendeesViewsSet(LoginRequiredMixin, SpyGuard, viewsets.ModelViewSet):
"""
API endpoint that allows FamiliesAttendees of a single Attendee in headers to be viewed or edited.
For example, if Alice, Bob & Charlie are in a family, passing Alice's attendee id in headers (key:
X-TARGET-ATTENDEE-ID) will return all 3 FamilyAttendee objects of Alice, Bob & Charlie. Also,
attaching Bob's FamilyAttendee id at the end of the endpoint will return Bob's FamilyAttendee only.

Note: If Dick is not in the family, passing Dick's attendee id in headers plus Bob's FamilyAttendee
id at the end of the endpoint will return nothing.
"""
serializer_class = FamilyAttendeeSerializer

def get_queryset(self):
target_attendee = get_object_or_404(Attendee, pk=self.request.META.get('HTTP_X_TARGET_ATTENDEE_ID'))
target_familyattendee_id = self.kwargs.get('pk')
target_attendee_familyattendees = FamilyAttendee.objects.filter(
family__in=target_attendee.families.all()
).order_by( # Todo: 20210516 order by attendee's family attendee display_order, such as order_by annotate()
'-family__created', 'role__display_order',
) # Todo: 20210515 add filter by start/finish for end users but not data-admins

if target_familyattendee_id:
return target_attendee_familyattendees.filter(pk=target_familyattendee_id)
else:
return target_attendee_familyattendees



# def update(self, request, *args, **kwargs): # from UpdateModelMixin
# print("hi 27 here is request: ")
# print(request)
# partial = kwargs.pop('partial', False)
# instance = self.get_object()
# serializer = self.get_serializer(instance, data=request.data, partial=partial)
# serializer.is_valid(raise_exception=True)
# self.perform_update(serializer)
#
# if getattr(instance, '_prefetched_objects_cache', None):
# # If 'prefetch_related' has been applied to a queryset, we need to
# # refresh the instance from the database.
# instance = self.get_object()
# serializer = self.get_serializer(instance)
#
# return Response(serializer.data)
#
# def perform_update(self, serializer): # from UpdateModelMixin
# serializer.save()
#
# def partial_update(self, request, *args, **kwargs): # from UpdateModelMixin
# print("hi 47 here is request: ")
# print(request)
# kwargs['partial'] = True
# return self.update(request, *args, **kwargs)


api_datagrid_data_familyattendees_viewset = ApiDatagridDataFamilyAttendeesViewsSet
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def get_context_data(self, **kwargs):
current_division_slug = self.kwargs.get('division_slug', None)
current_organization_slug = self.kwargs.get('organization_slug', None)
current_assembly_slug = self.kwargs.get('assembly_slug', None)
current_attendee_id = self.kwargs.get('attendee_id', self.request.user.attendee_uuid_str)
current_attendee_id = self.kwargs.get('attendee_id', self.request.user.attendee_uuid_str())
context.update({
'attendee_contenttype_id': ContentType.objects.get_for_model(Attendee).id,
'empty_image_link': f"{settings.STATIC_URL}images/empty.png",
Expand All @@ -38,11 +38,15 @@ def get_context_data(self, **kwargs):
'divisions_endpoint': '/whereabouts/api/user_divisions/',
'addresses_endpoint': '/whereabouts/api/all_addresses/',
'states_endpoint': '/whereabouts/api/all_states/',
'relations_endpoint': '/persons/api/all_relations/',
'attendee_families_endpoint': f"/persons/api/attendee_families/{current_attendee_id}/",
'attendings_endpoint': '/persons/api/attendee_attendings/',
'family_attendees_endpoint': "/persons/api/datagrid_data_familyattendees/",
'targeting_attendee_id': current_attendee_id,
'current_organization_slug': current_organization_slug,
'current_division_slug': current_division_slug,
'current_assembly_slug': current_assembly_slug,
'attendee_urn': f"/persons/{current_division_slug}/{current_assembly_slug}/datagrid_attendee_update_view/",
})
return context

Expand Down
9 changes: 7 additions & 2 deletions attendees/scripts/load_access_csv.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import csv, os, pytz, re
import csv, os, pytz, re, time
from datetime import datetime
from itertools import permutations
from glob import glob
Expand Down Expand Up @@ -181,6 +181,7 @@ def import_households(households, division1_slug, division2_slug):
print("\n\nRunning import_households:\n")
successfully_processed_count = 0 # households.line_num always advances despite of processing success
for household in households:
time.sleep(0.1) # bypass Todo: 20210516 order by attendee's family attendee display_order
try:
print('.', end='')
household_id = Utility.presence(household.get('HouseholdID'))
Expand Down Expand Up @@ -397,7 +398,11 @@ def import_attendees(peoples, division3_slug, data_assembly_slug, member_meet_sl
FamilyAttendee.objects.update_or_create(
family=family,
attendee=attendee,
defaults={'display_order': display_order, 'role': relation}
defaults={
'display_order': display_order,
'role': relation,
'start': '1900-01-01',
}
)

address_id = family.infos.get('access_household_values', {}).get('AddressID', 'missing')
Expand Down
5 changes: 5 additions & 0 deletions attendees/static/css/project.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,8 @@ img.attendee-photo-img:hover {
div.dx-popup-normal div.dx-box-item-content {
max-width: 98%;
} /* for editor items in dxform in dxpopup of attendingmeet to avoid scroll bar */

div.dx-fileuploader-input-label {
height: 0px;
visibility: hidden;
} /* to remove "or Drop File here" under dxFileUploader */
Loading