Skip to content

Commit

Permalink
Merge pull request #18 from xjlin0/attendees_without_assembly
Browse files Browse the repository at this point in the history
Attendees without assembly
  • Loading branch information
xjlin0 authored Jul 12, 2021
2 parents 69bb848 + d353752 commit c58d359
Show file tree
Hide file tree
Showing 15 changed files with 155 additions and 120 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ All libraries are included to facilitate offline development
- [x] [PR#5](https://github.com/xjlin0/attendees30/pull/5) Modify Attendee save method to combine/convert names by OpenCC to support searches in different text encoding, and retire db level full_name.
- [x] [PR#8](https://github.com/xjlin0/attendees30/pull/8) implement secret/private relation/past general
- [ ] Move attendee/attendees page out of data assembly -- some coworkers need to see all attendees of the organization, with a way to see only family members for general users
- [ ] remove all previous attendee edit testing pages
- [ ] remove attendee list page dependency of path params and take search params from user for assembly slug
- [x] [PR#17](https://github.com/xjlin0/attendees30/pull/17) remove all previous attendee edit testing pages
- [x] remove attendee list page dependency of path params and take search params from user for assembly slug
- [ ] rename and move attendees/attendee page, and show attendees based on auth groups
- [ ] Gathering list (server side processing with auto-generation)
- [ ] Attendance list (server side processing with auto-generation)
Expand Down
1 change: 1 addition & 0 deletions attendees/persons/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class FamilyAttendeeInline(admin.TabularInline):

class CategoryAdmin(admin.ModelAdmin):
readonly_fields = ['id', 'created', 'modified']
list_display_links = ('display_name',)
list_display = ('id', 'type', 'display_name', 'display_order', 'infos')


Expand Down
13 changes: 13 additions & 0 deletions attendees/persons/models/attendee.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from opencc import OpenCC

from django.db import models
from django.db.models import Q
from django.conf import settings
from django.core.exceptions import ValidationError
from django.contrib.contenttypes.fields import GenericRelation
Expand Down Expand Up @@ -87,6 +88,18 @@ def under_same_org_with(self, other_attendee_id):
return Attendee.objects.filter(pk=other_attendee_id, division__organization=self.division.organization).exists()
return False

def scheduling_attendees(self):
"""
:return: all attendees that can be scheduled by the self(included) based on relationships. For example, if a kid
specified "scheduler" is true in its parent relationship, when calling parent_attendee.scheduling_attendees(),
both the kid and the parent will be returned, means the parent can change/see schedule of the kid and the parent.
"""
return self.__class__.objects.filter(
Q(id=self.id)
|
Q(from_attendee__to_attendee__id=self.id, from_attendee__scheduler=True)
).distinct()

@cached_property
def parents_notifiers_names(self):
"""
Expand Down
3 changes: 2 additions & 1 deletion attendees/persons/models/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ def organization_infos():
"flags": {
"attendance_character_to_past_categories": {}
},
"groups_see_all_meets_attendees": [],
"contacts": {},
"counselor": [],
"data_admins": []
"data_admins": [],
}

@staticmethod
Expand Down
50 changes: 25 additions & 25 deletions attendees/persons/services/attendee_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def find_related_ones(current_user, target_attendee, querying_attendee_id, filte
else:
init_query = Q(division__organization=current_user.organization).add( # preventing browser hacks since
Q(is_removed=False), Q.AND)
final_query = init_query.add(AttendeeService.filter_parser(filters_list, None), Q.AND)
final_query = init_query.add(AttendeeService.filter_parser(filters_list, None, current_user), Q.AND)

if current_user.privileged:
return Attendee.objects.filter(final_query).order_by(
Expand All @@ -104,36 +104,35 @@ def find_related_ones(current_user, target_attendee, querying_attendee_id, filte
return target_attendee.related_ones.all()

@staticmethod
def by_datagrid_params(current_user_organization, assembly_slug, orderby_string, filters_list):
def by_datagrid_params(current_user, meets, orderby_string, filters_list):
"""
:param current_user_organization:
:param assembly_slug: attendee participated assembly. Exception: if assembly is in organization's all_access_assemblies, all attendee of the same org will be return
:param current_user:
:param meets: attendee participated assembly ids. Exception: if assembly is in organization's all_access_assemblies, all attendee of the same org will be return
:param orderby_string:
:param filters_list:
:return:
"""
orderby_list = AttendeeService.orderby_parser(orderby_string, assembly_slug)
orderby_list = AttendeeService.orderby_parser(orderby_string, meets, current_user)

init_query = Q(division__organization=current_user_organization) if assembly_slug in current_user_organization.infos['all_access_assemblies'] else Q(attendings__meets__assembly__slug=assembly_slug)
init_query = Q(division__organization=current_user.organization)
# Todo: need filter on attending_meet finish_date

final_query = init_query.add(AttendeeService.filter_parser(filters_list, assembly_slug), Q.AND)

return Attendee.objects.select_related().prefetch_related().annotate(
final_query = init_query.add(AttendeeService.filter_parser(filters_list, meets, current_user), Q.AND)
qs = Attendee.objects if current_user.can_see_all_organizational_meets_attendees() else current_user.attendee.scheduling_attendees()
return qs.select_related().prefetch_related().annotate(
attendingmeets=ArrayAgg('attendings__meets__slug', distinct=True),
).filter(final_query).filter(
division__organization=current_user_organization #Bugfix 20210517 limit org in init_query doesn't work.
).order_by(*orderby_list)
).filter(final_query).order_by(*orderby_list)

@staticmethod
def orderby_parser(orderby_string, assembly_slug):
def orderby_parser(orderby_string, meets, current_user):
"""
generates sorter (column or OrderBy Func) based on user's choice
:param orderby_string: JSON fetched from search params, will convert attendee.division to attendee__division
:param assembly_slug: assembly_slug
:param meets: assembly ids
:param current_user:
:return: a List of sorter for order_by()
"""
meet_sorters = {meet.slug: Func(F('attendingmeets'), function="'{}'=ANY".format(meet.slug)) for meet in Meet.objects.filter(assembly__slug=assembly_slug)}
meet_sorters = {meet.slug: Func(F('attendingmeets'), function="'{}'=ANY".format(meet.slug)) for meet in Meet.objects.filter(id__in=meets, assembly__division__organization=current_user.organization)}

orderby_list = [] # sort attendingmeets is [{"selector":"<<dataField value in DataGrid>>","desc":false}]
for orderby_dict in json.loads(orderby_string):
Expand All @@ -147,11 +146,12 @@ def orderby_parser(orderby_string, assembly_slug):
return orderby_list

@staticmethod
def filter_parser(filters_list, assembly_slug):
def filter_parser(filters_list, meets, current_user):
"""
A recursive method return Q function based on multi-level filter conditions
:param filters_list: a string of multi-level list of filter conditions
:param assembly_slug: assembly_slug
:param meets: assembly ids
:param current_user:
:return: Q function, could be an empty Q()
"""
and_string = Q.AND.lower()
Expand All @@ -162,24 +162,24 @@ def filter_parser(filters_list, assembly_slug):
raise Exception("Can't process both 'or'/'and' at the same level! please wrap them in separated lists.")
elif filters_list[1] == and_string:
and_list = [element for element in filters_list if element != and_string]
and_query = AttendeeService.filter_parser(and_list[0], assembly_slug)
and_query = AttendeeService.filter_parser(and_list[0], meets, current_user)
for and_element in and_list[1:]:
and_query.add(AttendeeService.filter_parser(and_element, assembly_slug), Q.AND)
and_query.add(AttendeeService.filter_parser(and_element, meets, current_user), Q.AND)
return and_query
elif filters_list[1] == or_string:
or_list = [element for element in filters_list if element != or_string]
or_query = AttendeeService.filter_parser(or_list[0], assembly_slug)
or_query = AttendeeService.filter_parser(or_list[0], meets, current_user)
for or_element in or_list[1:]:
or_query.add(AttendeeService.filter_parser(or_element, assembly_slug), Q.OR)
or_query.add(AttendeeService.filter_parser(or_element, meets, current_user), Q.OR)
return or_query
elif filters_list[1] == '=':
return Q(**{filters_list[0].replace('.', '__'): filters_list[2]})
elif filters_list[1] == 'contains':
return Q(**{AttendeeService.field_convert(filters_list[0], assembly_slug) + '__icontains': filters_list[2]})
return Q(**{AttendeeService.field_convert(filters_list[0], meets, current_user) + '__icontains': filters_list[2]})
return Q()

@staticmethod
def field_convert(query_field, assembly_slug):
def field_convert(query_field, meets, current_user):
"""
some of the values are calculated cell values, and need to convert back to db field for search
:return: string of fields in database
Expand All @@ -188,8 +188,8 @@ def field_convert(query_field, assembly_slug):
'self_phone_numbers': 'infos__contacts',
'self_email_addresses': 'infos__contacts',
}
if assembly_slug:
for meet in Meet.objects.filter(assembly__slug=assembly_slug):
if meets:
for meet in Meet.objects.filter(id__in=meets, assembly__division__organization=current_user.organization):
field_converter[meet.slug] = 'attendings__meets__display_name'

return field_converter.get(query_field, query_field).replace('.', '__')
Expand Down
8 changes: 4 additions & 4 deletions attendees/persons/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,18 +138,18 @@
),

path(
'<slug:division_slug>/<slug:assembly_slug>/datagrid_attendee_update_view/self',
'datagrid_attendee_update_view/self',
view=datagrid_attendee_update_view,
name='datagrid_attendee_update_self', # null attendee_id will be replaced by request.user's attendee_id
),
path(
'<slug:division_slug>/<slug:assembly_slug>/datagrid_attendee_update_view/new',
kwargs={'attendee_id': 'new', 'allowed_to_create_attendee': False},
'datagrid_attendee_update_view/new',
kwargs={'attendee_id': 'new', 'can_create_nonfamily_attendee': False},
view=datagrid_attendee_update_view,
name='datagrid_attendee_create_view', # for create non-family-attendee permission
),
path(
'<slug:division_slug>/<slug:assembly_slug>/datagrid_attendee_update_view/<str:attendee_id>',
'datagrid_attendee_update_view/<str:attendee_id>',
view=datagrid_attendee_update_view,
name='datagrid_attendee_update_view',
),
Expand Down
12 changes: 5 additions & 7 deletions attendees/persons/views/api/datagrid_data_attendees.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,16 @@ def get_queryset(self):
group = '[{"selector":"attendee.division","desc":false,"isExpanded":false}]'
:return: queryset ordered by query params from DataGrid
"""
current_user_organization = self.request.user.organization
orderby_string = self.request.query_params.get('sort', '[{"selector":"id","desc":false}]') # default order
assembly_slug = self.request.query_params.get('assembly')
meets_string = self.request.query_params.get('meets', '[]')
filters_list_string = self.request.query_params.get('filter', '[]')
filters_list = ast.literal_eval(filters_list_string) # Datagrid didn't send array in standard url params

return AttendeeService.by_datagrid_params(
current_user_organization=current_user_organization,
assembly_slug=assembly_slug,
current_user=self.request.user,
meets=ast.literal_eval(meets_string),
orderby_string=orderby_string,
filters_list=filters_list,
)
filters_list=ast.literal_eval(filters_list_string),
) # Datagrid can't send array in standard url params since filters can be dynamic nested arrays


api_datagrid_data_attendees_viewset = ApiDatagridDataAttendeesViewSet
55 changes: 23 additions & 32 deletions attendees/persons/views/page/datagrid_assembly_data_attendees.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from django.forms.models import model_to_dict
from django.http import Http404
from django.shortcuts import render
from attendees.occasions.models import Meet, Character
from django.db.models import F
from attendees.occasions.models import Meet, Assembly
from attendees.users.authorization import RouteGuard
import logging

Expand All @@ -24,47 +25,37 @@ class DatagridAssemblyDataAttendeesListView(RouteGuard, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Todo include user divisions and meets slugs in context
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)
family_attendances_menu = Menu.objects.filter(url_name='datagrid_user_organization_attendances').first()
available_meets = Meet.objects.filter(assembly__slug=current_assembly_slug).order_by('id')
available_characters = Character.objects.filter(assembly__slug=current_assembly_slug).order_by('display_order')
available_meets = Meet.objects.filter(assembly__division__organization=self.request.user.organization).annotate(assembly_name=F('assembly__display_name')).order_by('assembly_name').values('id', 'slug', 'display_name', 'assembly_name') # Todo 20210711 only coworkers can see all Meet, general users should only see what they attended
allowed_to_create_attendee = Menu.user_can_create_attendee(self.request.user)
context.update({
'current_organization_slug': current_organization_slug,
'current_division_slug': current_division_slug,
'current_assembly_slug': current_assembly_slug,
'family_attendances_urn': family_attendances_menu.urn if family_attendances_menu else None,
'available_meets': available_meets,
'available_meets_json': dumps([model_to_dict(m, fields=('id', 'slug', 'display_name')) for m in available_meets]),
'available_characters': available_characters,
'available_characters_json': dumps([model_to_dict(c, fields=('slug', 'display_name')) for c in available_characters]),
'available_meets_json': dumps(list(available_meets)),
'allowed_to_create_attendee': allowed_to_create_attendee,
'create_attendee_urn': f'/persons/{current_division_slug}/{current_assembly_slug}/datagrid_attendee_update_view/new',
'create_attendee_urn': f'/persons/datagrid_attendee_update_view/new',
})
return context

def render_to_response(self, context, **kwargs):
if self.request.user.belongs_to_divisions_of([context['current_division_slug']]):
if self.request.is_ajax():
pass
def render_to_response(self, context, **kwargs): # view only provides empty tables, it's API that needs to return valid data
# if self.request.user.belongs_to_divisions_of([context['current_division_slug']]):
if self.request.is_ajax():
pass

else:
# chosen_character_slugs = self.request.GET.getlist('characters', [])
# context.update({'chosen_character_slugs': chosen_character_slugs})
context.update({'divisions_endpoint': f"/whereabouts/api/user_divisions/"})
context.update({'teams_endpoint': f"/occasions/api/{context['current_division_slug']}/{context['current_assembly_slug']}/assembly_meet_teams/"})
context.update({'attendees_endpoint': f"/persons/api/{context['current_division_slug']}/{context['current_assembly_slug']}/assembly_meet_attendees/"})
context.update({'attendee_urn': f"/persons/{context['current_division_slug']}/{context['current_assembly_slug']}/datagrid_attendee_update_view/"})
context.update({'gatherings_endpoint': f"/occasions/api/{context['current_division_slug']}/{context['current_assembly_slug']}/assembly_meet_gatherings/"})
context.update({'characters_endpoint': f"/occasions/api/{context['current_division_slug']}/{context['current_assembly_slug']}/assembly_meet_characters/"})
context.update({'attendings_endpoint': f"/persons/api/{context['current_division_slug']}/{context['current_assembly_slug']}/data_attendings/"})
context.update({'attendances_endpoint': f"/occasions/api/{context['current_division_slug']}/{context['current_assembly_slug']}/assembly_meet_attendances/"})
return render(self.request, self.get_template_names()[0], context)
else:
time.sleep(2)
raise Http404('Have you registered any events of the organization?')
# chosen_character_slugs = self.request.GET.getlist('characters', [])
# context.update({'chosen_character_slugs': chosen_character_slugs})
context.update({'divisions_endpoint': f"/whereabouts/api/user_divisions/"})
# context.update({'teams_endpoint': f"/occasions/api/{context['current_division_slug']}/{context['current_assembly_slug']}/assembly_meet_teams/"})
# context.update({'attendees_endpoint': f"/persons/api/{context['current_division_slug']}/{context['current_assembly_slug']}/assembly_meet_attendees/"})
context.update({'attendee_urn': f"/persons/datagrid_attendee_update_view/"})
# context.update({'gatherings_endpoint': f"/occasions/api/{context['current_division_slug']}/{context['current_assembly_slug']}/assembly_meet_gatherings/"})
# context.update({'characters_endpoint': f"/occasions/api/{context['current_division_slug']}/{context['current_assembly_slug']}/assembly_meet_characters/"})
# context.update({'attendings_endpoint': f"/persons/api/{context['current_division_slug']}/{context['current_assembly_slug']}/data_attendings/"})
# context.update({'attendances_endpoint': f"/occasions/api/{context['current_division_slug']}/{context['current_assembly_slug']}/assembly_meet_attendances/"})
return render(self.request, self.get_template_names()[0], context)
# else:
# time.sleep(2)
# raise Http404('Have you registered any events of the organization?')

# def get_attendances(self, args):
# return []
Expand Down
Loading

0 comments on commit c58d359

Please sign in to comment.