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

Show duplicated attendees #28

Merged
merged 13 commits into from
Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,9 @@ DJANGO_DEFAULT_FROM_EMAIL=fake@email.com
- [x] [PR#22](https://github.com/xjlin0/attendees30/pull/22) can gathering generation automatic?
- [x] [PR#23](https://github.com/xjlin0/attendees30/pull/23) sorting & grouping by server side processing
- [x] data [db backup/restore](https://cookiecutter-django.readthedocs.io/en/latest/docker-postgres-backups.html) to survive new releases and migrations
- [ ] Add Attendee+ buttons in above pages should deduplicate before creation by providing existing names for users to choose
- [x] Add Attendee+ buttons in above pages should deduplicate before creation by providing existing names for users to choose
- [x] [PR#24](https://github.com/xjlin0/attendees30/pull/24) fix self attendee page error
- [ ] from Attendee detail and attendee list page
- [x] [PR#28](https://github.com/xjlin0/attendees30/pull/28) from Attendee detail and attendee list page
- [ ] AttendingMeet list (server side processing)
- [ ] new attendance datagrid filtered by meets and date ranges
- [ ] auto-generation of AttendingMeet by django-schedule with certain Past
Expand All @@ -300,6 +300,7 @@ DJANGO_DEFAULT_FROM_EMAIL=fake@email.com
- [ ] Export pdf
- [ ] directory booklet
- [ ] mail labels (avery template) or printing envelops
- [ ] Todo: 20210517 When creating FamilyAttendee, also auto create relationships among families such as siblings, etc
- [ ] i18n Translation on model data, django-parler maybe?

</details>
Expand All @@ -311,4 +312,15 @@ DJANGO_DEFAULT_FROM_EMAIL=fake@email.com

- [ ] for ordinary users:
-[ ] even as scheduler seeing other's attendee detail view, the joined meet doesn't show group name (i.e. Hagar cannot see Ishmael in "the Rock")
-[ ] If manager A checked "secret shared with you" for a model with "share secret with you" and uniq constrain such as Relationship, manager B can't see it (expected) and creating another relationship will fail due to uniq constrain (not expected). If relating uniq constraint, after manager B create the very same relationship, manager A will see duplicated relationship.
</details>

## Design decisions:

<details>
<summary>Click to expand all</summary>

- [ ] for all users:
-[ ] show_secret in Relationship/Past are based on attendee id instead of user id, since secret relationship exists regardless of user accounts. For example, kid X bullied kid Y, and counsellors can configure kids parents & teachers to see it regardless of parent/teachers user accounts existence or switches.
-[ ] There is no decision on display notes/infos of Relationships/FamilyAttendee/Past. Ordinary end users can see their families, so role-dependent showing/hiding notes columns need designs, such as storing allowed columns in Menu.infos read by User.allowed_url_names()? Currently UI only expose separated sections on Past which conditionally show to ordinary end users.
</details>
2 changes: 1 addition & 1 deletion attendees/persons/models/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def attendee_infos():

@staticmethod
def relationship_infos():
return {"show_secret": {}, "comment": None, "body": None}
return {"show_secret": {}, "updated_by": {}, "comment": None, "body": None}

@staticmethod
def forever(): # 1923 years from now
Expand Down
34 changes: 18 additions & 16 deletions attendees/persons/serializers/family_attendee_serializer.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from rest_framework import serializers

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


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

class Meta:
model = FamilyAttendee
Expand All @@ -21,15 +21,16 @@ def create(self, 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', {})
new_attendee_id = 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,
)
print("hi 27 here is validated_data: ", validated_data)
if new_attendee_id:
# attendee, attendee_created = Attendee.objects.update_or_create(
# id=new_attendee_data.get('id'),
# defaults=new_attendee_data,
# )
attendee = Attendee.objects.get(pk=new_attendee_id)
validated_data['attendee'] = attendee
# Todo: 20210517 create relationships among families such as siblings, etc
obj, created = FamilyAttendee.objects.update_or_create(
Expand All @@ -44,19 +45,20 @@ def update(self, instance, validated_data):

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

new_attendee_id = validated_data.get('attendee', {})
print("hi 49 here is validated_data: ", validated_data)
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,
)
if new_attendee_id:
# attendee, attendee_created = Attendee.objects.update_or_create(
# id=instance.attendee.id,
# defaults=new_attendee_data,
# )
attendee = Attendee.objects.get(pk=new_attendee_id)
validated_data['attendee'] = attendee
# else:
# validated_data['attendee'] = instance.attendee
Expand Down
17 changes: 8 additions & 9 deletions attendees/persons/services/attendee_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,22 +104,21 @@ 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, meets, orderby_string, filters_list):
def by_datagrid_params(current_user, meets, orderby_string, filters_list, include_dead):
"""
: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:
:param include_dead:
:return:
"""
orderby_list = AttendeeService.orderby_parser(orderby_string, meets, current_user)
init_query_q = {'division__organization': current_user.organization, 'is_removed': False}
if not include_dead:
init_query_q['deathday__isnull'] = True

init_query = Q(
division__organization=current_user.organization,
deathday__isnull=True,
is_removed=False,
)
# Todo: need filter on attending_meet finish_date
init_query = Q(**init_query_q) # Todo: need filter on attending_meet finish_date

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()
Expand Down Expand Up @@ -259,8 +258,8 @@ def destroy_with_associations(attendee):
old_file = Path(old_photo.path)
old_file.unlink(missing_ok=True)

if hasattr(attendee, 'user'):
attendee_user = attendee.user
attendee_user = attendee.user
if attendee_user:
attendee.delete()
attendee_user.delete()
else:
Expand Down
4 changes: 2 additions & 2 deletions attendees/persons/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,9 @@
),
path(
'attendee/new',
kwargs={'attendee_id': 'new', 'show_create_nonfamily_attendee': False},
kwargs={'attendee_id': 'new', 'show_create_attendee': False},
view=attendee_update_view,
name='attendee_create_view', # for create non-family-attendee permission
name='attendee_create_view', # for create attendee permission
),
path(
'attendee/<str:attendee_id>',
Expand Down
13 changes: 10 additions & 3 deletions attendees/persons/views/api/all_relations.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import ast
from django.contrib.auth.mixins import LoginRequiredMixin

from django.db.models import Q
from rest_framework import viewsets

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


class ApiAllRelationsViewsSet(LoginRequiredMixin, viewsets.ModelViewSet):
"""
API endpoint that allows Relation(Role) to be viewed or edited.
API endpoint that allows Relation(Role) to be viewed or edited. It's public and not org limited.
"""
serializer_class = RelationSerializer

def get_queryset(self):
relation_id = self.request.query_params.get('relation_id')
filters_list_string = self.request.query_params.get('filter', '[]')
filters_list = ast.literal_eval(filters_list_string) # copied from ApiDatagridDataAttendeesViewSet

if relation_id:
return Relation.objects.filter(pk=relation_id)
else:
return Relation.objects.order_by('display_order')
init_query = Q(is_removed=False)
final_query = init_query.add(AttendeeService.filter_parser(filters_list, None, self.request.user), Q.AND)
return Relation.objects.filter(final_query).order_by('display_order')


api_all_relations_viewset = ApiAllRelationsViewsSet
86 changes: 80 additions & 6 deletions attendees/persons/views/api/attendee_relationships.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import time
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from django.shortcuts import get_object_or_404
from rest_framework import viewsets
from rest_framework.exceptions import PermissionDenied
Expand Down Expand Up @@ -31,18 +32,91 @@ def get_queryset(self):

target_attendee = get_object_or_404(Attendee, pk=self.request.META.get('HTTP_X_TARGET_ATTENDEE_ID'))
target_relationship_id = self.kwargs.get('pk')
requester_permission = {'infos__show_secret__' + self.request.user.attendee_uuid_str(): True}

if target_relationship_id:
return Relationship.objects.filter(
pk=target_relationship_id,
to_attendee__division__organization=target_attendee.division.organization,
is_removed=False,
Q(pk=target_relationship_id),
Q(to_attendee__division__organization=target_attendee.division.organization),
Q(is_removed=False),
(Q(infos__show_secret={})
|
Q(infos__show_secret__isnull=True)
|
Q(**requester_permission)),
)
else:
return Relationship.objects.filter(
from_attendee=target_attendee,
to_attendee__division__organization=target_attendee.division.organization,
is_removed=False,
Q(from_attendee=target_attendee),
Q(to_attendee__division__organization=target_attendee.division.organization),
Q(is_removed=False),
(Q(infos__show_secret={})
|
Q(infos__show_secret__isnull=True)
|
Q(**requester_permission)),
)

def perform_create(self, serializer): #SpyGuard ensured requester & target_attendee belongs to the same org.
"""
Reason for special create:
If manager A checked "secret shared with you" for a Relationship, manager B
can't see it (expected) and creating the same relationship will fail due to uniq
constrain (not expected). If relaxing uniq constraint, after manager B creating
the very same relationship, manager A will see duplicated relationship. Instead we
will add manager B id in secret shared with you of infos when manager B create it.
"""
current_user_attendee_id = self.request.user.attendee_uuid_str()
existing_relationship = Relationship.objects.filter(
from_attendee=serializer.validated_data['from_attendee'],
to_attendee=serializer.validated_data['to_attendee'],
relation=serializer.validated_data['relation'],
is_removed=False,
).first()
if existing_relationship and existing_relationship.infos.get('show_secret'): # current manager can't see existing record & try to create one, means existing record must be secret
existing_relationship.infos['show_secret'][current_user_attendee_id] = True
existing_relationship.infos['updated_by'][current_user_attendee_id] = Utility.now_with_timezone().isoformat()
existing_relationship.save() # manager's new data is intentionally discarded to show previous data
else:
serializer.validated_data['infos']['updated_by'] = {current_user_attendee_id: Utility.now_with_timezone().isoformat()}
serializer.save()

def perform_update(self, serializer):
"""
Reason for special update:
For a public relationship, if a manager add him/herself in secret shared with you of
infos, all updater should be added too, or updater won't see it.

if manager A&B both in secret shared with you of infos, either manager can make it public.
"""
existing_relationship = get_object_or_404(Relationship, pk=self.kwargs.get('pk'))
if 'show_secret' in serializer.validated_data.get('infos', {}):
if serializer.validated_data['infos']['show_secret']: # user is checking "secret shared with you"
for updater_attendee_id in existing_relationship.infos.get('updated_by', {}):
serializer.validated_data['infos']['show_secret'][updater_attendee_id] = True
existing_relationship.infos['show_secret'][updater_attendee_id] = True
for new_attendee_id in serializer.validated_data['infos']['show_secret']:
existing_relationship.infos['show_secret'][new_attendee_id] = True
else: # user is unchecking "secret shared with you", making it public
existing_relationship.infos['show_secret'] = {}
serializer.validated_data['infos'] = existing_relationship.infos
serializer.validated_data['infos']['updated_by'][self.request.user.attendee_uuid_str()] = Utility.now_with_timezone().isoformat()
serializer.save()

def perform_destroy(self, instance):
"""
Reason for special delete:
When both managers are in secret shared with you of infos, when manager A deletes
such records, it will only remove manager A from secret shared with you of infos.
"""
current_user_attendee_id = self.request.user.attendee_uuid_str()
instance.infos['updated_by'][current_user_attendee_id] = Utility.now_with_timezone().isoformat()
if len(instance.infos.get('show_secret', {})) > 1: # manager must able to see record before delete
del instance.infos['show_secret'][current_user_attendee_id]
instance.save()
else:
instance.save()
instance.delete()


api_attendee_relationships_viewset = ApiAttendeeRelationshipsViewSet
1 change: 1 addition & 0 deletions attendees/persons/views/api/categorized_pasts.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def get_queryset(self):
qs = target_attendee.pasts.filter(
Q(organization=self.request.user.organization),
Q(category__type=category__type),
Q(is_removed=False),
( Q(infos__show_secret={})
|
Q(infos__show_secret__isnull=True)
Expand Down
3 changes: 2 additions & 1 deletion attendees/persons/views/api/datagrid_data_attendees.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ def get_queryset(self):
orderby_string = self.request.query_params.get('sort', '[{"selector":"id","desc":false}]') # default order
meets_string = self.request.query_params.get('meets', '[]')
filters_list_string = self.request.query_params.get('filter', '[]')

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


Expand Down
Loading