diff --git a/README.md b/README.md index b1f46abc..dc2ab531 100644 --- a/README.md +++ b/README.md @@ -256,11 +256,13 @@ DJANGO_DEFAULT_FROM_EMAIL=fake@email.com - [x] [PR#16](https://github.com/xjlin0/attendees30/pull/16) delete function for human error - [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 + - [ ] some relationship may be internal and only shows to cowokers/admin, in category/boolean/infos column? - [ ] Rich format of note for Past on UI? - [x] 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 - [x] [PR#17](https://github.com/xjlin0/attendees30/pull/17) remove all previous attendee edit testing pages - [x] [PR#18](https://github.com/xjlin0/attendees30/pull/18) remove attendee list page dependency of path params and take search params from user for assembly slug - [x] [PR#19](https://github.com/xjlin0/attendees30/pull/19) rename and move attendees/attendee page, and show attendees based on auth groups + - [ ] [PR#26](https://github.com/xjlin0/attendees30/pull/26) make special Past as Meet to be shown in Attendees, also creating such Past will result in AttendingMeet creation - [x] Gathering list (server side processing with auto-generation) - [x] [PR#20](https://github.com/xjlin0/attendees30/pull/20) gatherings datagrid filtered by meets and date ranges - [x] [PR#21](https://github.com/xjlin0/attendees30/pull/21) django-schedule with Meet @@ -269,14 +271,14 @@ DJANGO_DEFAULT_FROM_EMAIL=fake@email.com - [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] [PR#24](https://github.com/xjlin0/attendees30/pull/24) fix self attendee page error - - [ ] [PR#25](https://github.com/xjlin0/attendees30/pull/25) from Attendee detail and attendee list page + - [ ] from Attendee detail and attendee list page - [ ] AttendingMeet list (server side processing) - - [ ] [PR#26](https://github.com/xjlin0/attendees30/pull/26) new attendance datagrid filtered by meets and date ranges - - [ ] [PR#27](https://github.com/xjlin0/attendees30/pull/27) auto-generation of AttendingMeet by django-schedule with + - [ ] new attendance datagrid filtered by meets and date ranges + - [ ] auto-generation of AttendingMeet by django-schedule with certain Past - [ ] Attendance list (server side processing with auto-generation) - - [ ] [PR#28](https://github.com/xjlin0/attendees30/pull/28) new attendance datagrid filtered by meets and date ranges - - [ ] [PR#29](https://github.com/xjlin0/attendees30/pull/29) auto-generation attendance by attending meet and recent attendance status - - [ ] [PR#30](https://github.com/xjlin0/attendees30/pull/30) member list (attendance level with editing category) + - [ ] new attendance datagrid filtered by meets and date ranges + - [ ] auto-generation attendance by attending meet and recent attendance status + - [ ] member list (attendance level with editing category) - [ ] Create roaster page (no real-time update for multiple coworkers in v1) - [ ] Coworker roaster on phone/web, X: characters, Y: dates(gatherings) - [ ] Division specific menu links, such as including selected meets in the search params @@ -287,9 +289,10 @@ DJANGO_DEFAULT_FROM_EMAIL=fake@email.com - [ ] each model level version - [ ] document aggregation level version - [ ] upgrade to Django 3.1, 3.2LTS or 4, depends on Django Cookie-cutter's support of DEFAULT_AUTO_FIELD - -[ ] use Django JSONField instead of Postgres JSONField - -[ ] With Django Cookie-cutter, decide async or not (uvicorn high CPU usage, but web_socket can be only with use_async) + -[ ] 3.1: use Django JSONField instead of Postgres JSONField + -[ ] With Django Cookie-cutter, decide async or not (uvicorn high CPU usage in Mac only, but web_socket can be only with use_async) - [ ] deploy to AWS EC2 + - [x] [PR#25](https://github.com/xjlin0/attendees30/pull/25) ensure libraries loaded other than MacOS - [ ] Export pdf - [ ] directory booklet - [ ] mail labels (avery template) or printing envelops diff --git a/attendees/context_processors.py b/attendees/context_processors.py index b1b27327..3de5ed41 100644 --- a/attendees/context_processors.py +++ b/attendees/context_processors.py @@ -20,13 +20,13 @@ def common_variables(request): # TODO move organization info to view ).distinct() if request.user.is_authenticated and user_organization: user_organization = request.user.organization - user_organization_name = user_organization.display_name + user_organization_name = user_organization.infos['acronym'] or user_organization.display_name user_organization_name_slug = user_organization.slug return { 'timezone_name': datetime.now(timezone(parse.unquote(tzname))).tzname(), 'user_organization_name': user_organization_name, 'user_organization_name_slug': user_organization_name_slug, 'user_api_allowed_url_name': json.dumps({name: True for name in request.user.allowed_url_names()} if hasattr(request.user, 'allowed_url_names') else {}), - 'user_attendee_id': request.user.attendee_uuid_str() if hasattr(request.user, 'attendee_uuid_str') else None, # could be different when admin browser others + 'user_attendee_id': request.user.attendee_uuid_str() if hasattr(request.user, 'attendee_uuid_str') else None, # Anonymous User does not have attendee_uuid_str, also could be different when admin browser others 'main_menus': main_menus, } diff --git a/attendees/occasions/admin.py b/attendees/occasions/admin.py index 8db888ed..85849125 100644 --- a/attendees/occasions/admin.py +++ b/attendees/occasions/admin.py @@ -119,11 +119,11 @@ class MeetAdmin(admin.ModelAdmin): readonly_fields = ['id', 'created', 'modified'] fieldsets = ( (None, {"fields": (tuple(['start', 'finish', 'slug']), - tuple(['display_name', 'infos', 'shown_audience', 'audience_editable']), + tuple(['display_name', 'major_character', 'infos', 'shown_audience', 'audience_editable']), tuple(['site_type', 'assembly', 'site_id']), tuple(['id', 'created', 'modified']), ), - 'classes': ['hijack', ], + # 'classes': ['hijack', ], # the above entire fieldset }), ) diff --git a/attendees/occasions/migrations/0002_assembly.py b/attendees/occasions/migrations/0002_assembly.py index efdba200..31aae8f0 100644 --- a/attendees/occasions/migrations/0002_assembly.py +++ b/attendees/occasions/migrations/0002_assembly.py @@ -36,7 +36,7 @@ class Migration(migrations.Migration): options={ 'db_table': 'occasions_assemblies', 'verbose_name_plural': 'Assemblies', - 'ordering': ('display_order',), + 'ordering': ('division', 'display_order'), }, bases=(models.Model, attendees.persons.models.utility.Utility), ), diff --git a/attendees/occasions/migrations/0004_character.py b/attendees/occasions/migrations/0004_character.py index 182700f5..a0884ec7 100644 --- a/attendees/occasions/migrations/0004_character.py +++ b/attendees/occasions/migrations/0004_character.py @@ -29,7 +29,7 @@ class Migration(migrations.Migration): ], options={ 'db_table': 'occasions_characters', - 'ordering': ['display_order'], + 'ordering': ['assembly', 'display_order'], }, bases=(models.Model, attendees.persons.models.utility.Utility), ), diff --git a/attendees/occasions/migrations/0005_meet.py b/attendees/occasions/migrations/0005_meet.py index 861c247c..9590a3d3 100644 --- a/attendees/occasions/migrations/0005_meet.py +++ b/attendees/occasions/migrations/0005_meet.py @@ -28,6 +28,7 @@ class Migration(migrations.Migration): ('site_type', models.ForeignKey(help_text='site: django_content_type id for table name', on_delete=models.SET(0), to='contenttypes.ContentType')), ('site_id', models.CharField(default='0', max_length=36)), ('assembly', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='occasions.Assembly')), + ('major_character', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='occasions.Character')), ('slug', models.SlugField(max_length=50, unique=True)), ('display_name', models.CharField(blank=True, null=True, db_index=True, help_text='The Rock, Little Foot, singspiration, A/V control, etc.', max_length=50)), ('infos', JSONField(blank=True, default=dict, help_text='Example: {"info": "...", "url": "https://..."}. Please keep {} here even no data', null=True)), diff --git a/attendees/occasions/models/assembly.py b/attendees/occasions/models/assembly.py index 88e83d10..d09a5817 100644 --- a/attendees/occasions/models/assembly.py +++ b/attendees/occasions/models/assembly.py @@ -30,7 +30,7 @@ def get_absolute_url(self): class Meta: db_table = 'occasions_assemblies' verbose_name_plural = 'Assemblies' - ordering = ('display_order',) + ordering = ('division', 'display_order',) indexes = [ GinIndex(fields=['infos'], name='assembly_infos_gin', ), ] diff --git a/attendees/occasions/models/character.py b/attendees/occasions/models/character.py index c950bdf6..8537ac57 100644 --- a/attendees/occasions/models/character.py +++ b/attendees/occasions/models/character.py @@ -22,7 +22,7 @@ def get_absolute_url(self): class Meta: db_table = 'occasions_characters' - ordering = ['display_order'] + ordering = ['assembly', 'display_order'] def __str__(self): return '%s %s %s' % (self.display_name, self.type, self.info or '') diff --git a/attendees/occasions/models/meet.py b/attendees/occasions/models/meet.py index 327ceaf7..d61b06a8 100644 --- a/attendees/occasions/models/meet.py +++ b/attendees/occasions/models/meet.py @@ -22,6 +22,7 @@ class Meet(TimeStampedModel, SoftDeletableModel, Utility): id = models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID') assembly = models.ForeignKey('occasions.Assembly', null=True, blank=True, on_delete=models.SET_NULL) attendings = models.ManyToManyField('persons.Attending', through='persons.AttendingMeet', related_name="attendings") + major_character = models.ForeignKey('occasions.Character', null=True, blank=True, on_delete=models.SET_NULL) shown_audience = models.BooleanField('show AttendingMeet to participant?', null=False, blank=False, db_index=True, default=True, help_text="[some meets are only for internal records] show the AttendingMeet to attendee?") audience_editable = models.BooleanField('participant can edit AttendingMeet?', null=False, blank=False, default=True, help_text="[some meets are editable only by coworkers] participant can edit AttendingMeet?") start = models.DateTimeField(null=False, blank=False, default=Utility.now_with_timezone) diff --git a/attendees/persons/admin.py b/attendees/persons/admin.py index 7e2e2a22..4f62b455 100644 --- a/attendees/persons/admin.py +++ b/attendees/persons/admin.py @@ -48,7 +48,7 @@ class PastAdmin(admin.ModelAdmin): def get_queryset(self, request): qs = super().get_queryset(request) - counseling_category = Category.objects.get(type='note', display_name=Past.COUNSELING) + counseling_category = Category.objects.filter(type='note', display_name=Past.COUNSELING).first() if request.resolver_match.func.__name__ == 'changelist_view': messages.warning(request, 'Not all, but only those records accessible to you will be listed here.') @@ -170,7 +170,7 @@ class NoteAdmin(SummernoteModelAdmin): def get_queryset(self, request): # even super user cannot see all in DjangoAdmin qs = super().get_queryset(request) - counseling_category = Category.objects.get(type='note', display_name=Note.COUNSELING) + counseling_category = Category.objects.filter(type='note', display_name=Note.COUNSELING).first() if request.resolver_match.func.__name__ == 'changelist_view': messages.warning(request, 'Not all, but only those notes accessible to you will be listed here.') diff --git a/attendees/persons/apps.py b/attendees/persons/apps.py index c62af638..92645c5d 100644 --- a/attendees/persons/apps.py +++ b/attendees/persons/apps.py @@ -3,3 +3,7 @@ class PersonsConfig(AppConfig): name = 'attendees.persons' + + def ready(self): + # importing signal handlers # https://docs.djangoproject.com/en/dev/topics/signals/#preventing-duplicate-signals + import attendees.persons.signals diff --git a/attendees/persons/models/attendee.py b/attendees/persons/models/attendee.py index 6898299a..3b94f48c 100644 --- a/attendees/persons/models/attendee.py +++ b/attendees/persons/models/attendee.py @@ -170,8 +170,8 @@ def save(self, *args, **kwargs): name2 = f"{self.last_name2 or ''}{self.first_name2 or ''}".strip() both_names = f"{name} {name2}".strip() self.infos['names']['original'] = both_names - self.infos['names']['romanization'] = unidecode(both_names) # remove accents & get phonetic - if self.division.organization.infos.get('flags', {}).get('opencc_convert'): # Let search work in either language + self.infos['names']['romanization'] = unidecode(both_names).strip() # remove accents & get phonetic + if self.division.organization.infos.get('settings', {}).get('opencc_convert'): # Let search work in either language s2t_converter = opencc.OpenCC('s2t.json') t2s_converter = opencc.OpenCC('t2s.json') self.infos['names']['traditional'] = s2t_converter.convert(both_names) diff --git a/attendees/persons/models/utility.py b/attendees/persons/models/utility.py index 93fb7f00..608c0cea 100644 --- a/attendees/persons/models/utility.py +++ b/attendees/persons/models/utility.py @@ -48,8 +48,10 @@ def default_infos(): def organization_infos(): return { "default_time_zone": settings.CLIENT_DEFAULT_TIME_ZONE, - "flags": { - "attendance_character_to_past_categories": {} + "settings": { + "attendee_to_attending": True, + "past_category_to_attendingmeet_meet": {}, + "attendingmeet_meet_to_past_category": {}, }, "groups_see_all_meets_attendees": [], "contacts": {}, @@ -141,6 +143,29 @@ def get_location(eventrelation): return None + def update_or_create_last(klass, order_key='pk', update=True, defaults=None, filters=None): + """ + Sililar to update_or_create(), it'll search by the filters dictionary, get the last by + order_by, update its values specified by defaults dictionary, return created and obj + + :param order_key: order by condition. + :param update: boolean: do you want to update the object if any matched? + :param defaults: new values will be updated to the matched object + :param filters: + :return: tuple of updated/created object, created boolean + """ + obj = klass.objects.filter(**filters).order_by(order_key).last() + created = False + if obj: + if update: + for key, value in defaults.items(): + setattr(obj, key, value) + else: + filters.update(defaults) + obj = klass(**filters) + created = True + obj.save() + return obj, created # @property # def notes(self): diff --git a/attendees/persons/signals.py b/attendees/persons/signals.py new file mode 100644 index 00000000..027b6729 --- /dev/null +++ b/attendees/persons/signals.py @@ -0,0 +1,106 @@ +from django.contrib.contenttypes.models import ContentType +from django.db.models.signals import post_save +from django.dispatch import receiver + +from attendees.occasions.models import Meet +from attendees.persons.models import Attendee, Attending, AttendingMeet, Category, Past, Utility + + +@receiver(post_save, sender=Past) +def post_save_handler_for_past_to_create_attendingmeet(sender, **kwargs): + """ + To let user easily spot certain attendee attributes, here is automatic creation + of AttendingMeet after creating Past of certain categories in Organization.infos + + :param sender: sender Class, Past + :param kwargs: + :return: None + """ + if not kwargs.get('raw') and kwargs.get('created'): # to skip extra creation in loaddata seed + created_past = kwargs.get('instance') + organization = created_past.organization + category_id = str(created_past.category.id) # json can only have string as key, not numbers + meet_id = organization.infos.get('settings', {}).get('past_category_to_attendingmeet_meet', {}).get(category_id) + if meet_id and 'importer' not in created_past.infos.get('comment', ''): # skip for access importer + meet = Meet.objects.filter(pk=meet_id).first() + if meet: + target_attendee = created_past.subject + first_attending = target_attendee.attendings.first() + if first_attending: + defaults = {'character': meet.major_character, 'finish': Utility.forever()} + if created_past.when: + defaults['start'] = created_past.when + Utility.update_or_create_last( + AttendingMeet, + update=False, + filters={'meet': meet, 'attending': first_attending, 'is_removed': False}, + defaults=defaults, + ) + + +@receiver(post_save, sender=AttendingMeet) +def post_save_handler_for_attendingmeet_to_create_past(sender, **kwargs): + """ + To let coworker easily spot certain attendee Past, here is automatic creation + of Past after creating AttendingMeet of certain meets in Organization.infos + + :param sender: sender Class, AttendingMeet + :param kwargs: + :return: None + """ + if not kwargs.get('raw') and kwargs.get('created'): # to skip extra creation in loaddata seed + created_attendingmeet = kwargs.get('instance') + organization = created_attendingmeet.meet.assembly.division.organization + meet_id = str(created_attendingmeet.meet.id) + category_id = organization.infos.get('settings', {}).get('attendingmeet_meet_to_past_category', {}).get(meet_id) + + if category_id and 'importer' not in created_attendingmeet.category: # skip for access importer + category = Category.objects.filter(pk=category_id).first() + if category: + target_attendee = created_attendingmeet.attending.attendee + attendee_content_type = ContentType.objects.get_for_model(target_attendee) + defaults = { + 'display_name': 'activity added', + 'when': None, # AttendingMeet's start may not be actual date + 'infos': { + **Utility.relationship_infos(), + 'comment': 'Auto created by AttendingMeet importer signal', + }, + } + + Utility.update_or_create_last( + Past, + update=False, # order_key='modified', # Past id is UUID and out of order + filters={'organization': organization, 'content_type': attendee_content_type, 'object_id': target_attendee.id, 'category': category, 'is_removed': False}, + defaults=defaults, + ) + + +@receiver(post_save, sender=Attendee) +def post_save_handler_for_attendee_to_attending(sender, **kwargs): + """ + To let coworker easily create AttendingMeet/Past, here is automatic creation + of Attending after creating Attendee by settings from Organization.infos + + :param sender: sender Class, Attendee + :param kwargs: + :return: None + """ + if not kwargs.get('raw') and kwargs.get('created'): # to skip extra creation in loaddata seed + created_attendee = kwargs.get('instance') + organization = created_attendee.division.organization # Maybe 0 in access importer + + if 'importer' not in created_attendee.infos.get('created_reason', '') and organization.infos.get('settings', {}).get('attendee_to_attending'): # skip for access importer + defaults = { + 'category': 'auto-created', + 'infos': { + 'created_reason': 'Auto created by Attendee creation', + }, + } + + Utility.update_or_create_last( + Attending, + update=False, # order_key='modified', # Past id is UUID and out of order + filters={'attendee': created_attendee, 'is_removed': False}, + defaults=defaults, + ) diff --git a/attendees/persons/views/api/categorized_pasts.py b/attendees/persons/views/api/categorized_pasts.py index 8ccb9bf6..acfbac21 100644 --- a/attendees/persons/views/api/categorized_pasts.py +++ b/attendees/persons/views/api/categorized_pasts.py @@ -1,7 +1,5 @@ import time from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.contenttypes.models import ContentType -from django.http import Http404 from django.shortcuts import get_object_or_404 from rest_framework import viewsets from django.db.models import Q diff --git a/attendees/scripts/load_access_csv.py b/attendees/scripts/load_access_csv.py index 37e80b6d..ae1048f8 100644 --- a/attendees/scripts/load_access_csv.py +++ b/attendees/scripts/load_access_csv.py @@ -11,7 +11,7 @@ from attendees.occasions.models import Assembly, Meet, Character, Gathering, Attendance from attendees.persons.models import Utility, GenderEnum, Family, Relation, Attendee, FamilyAttendee, \ - Relationship, Registration, Attending, AttendingMeet + Relationship, Registration, Attending, AttendingMeet, Past, Category from attendees.users.admin import User from attendees.whereabouts.models import Place, Division @@ -26,10 +26,13 @@ def import_household_people_address( data_assembly_slug, member_meet_slug, directory_meet_slug, - member_character_slug, - directory_character_slug, + baptized_meet_slug, + # member_character_slug, + # directory_character_slug, roaster_meet_slug, - data_general_character_slug, + believer_meet_slug, + # data_general_character_slug, + # data_baptisee_character_slug, ): """ Entry function of entire importer, it execute importers in sequence and print out results. @@ -42,10 +45,9 @@ def import_household_people_address( :param data_assembly_slug: key of data_assembly :param member_meet_slug: key of member_gathering :param directory_meet_slug: key of directory_gathering - :param member_character_slug: key of member_character - :param directory_character_slug: key of directory_character + :param baptized_meet_slug: key of baptized_meet_slug :param roaster_meet_slug: key of roaster_meet_slug - :param data_general_character_slug: key of data_general_character_slug + :param believer_meet_slug: key of believer_meet_slug :return: None, but print out importing status and write to Attendees db (create or update) """ if User.objects.count() < 1: @@ -67,10 +69,10 @@ def import_household_people_address( initial_relationship_count = Relationship.objects.count() upserted_address_count = import_addresses(addresses, california, division1_slug) upserted_household_id_count = import_households(households, division1_slug, division2_slug) - upserted_attendee_count, photo_import_results = import_attendees(peoples, division3_slug, data_assembly_slug, member_meet_slug, member_character_slug, roaster_meet_slug, data_general_character_slug) + upserted_attendee_count, photo_import_results = import_attendees(peoples, division3_slug, data_assembly_slug, member_meet_slug, baptized_meet_slug, roaster_meet_slug, believer_meet_slug) if upserted_address_count and upserted_household_id_count and upserted_attendee_count: - upserted_relationship_count = reprocess_directory_emails_and_family_roles(data_assembly_slug, directory_meet_slug, directory_character_slug) + upserted_relationship_count = reprocess_directory_emails_and_family_roles(data_assembly_slug, directory_meet_slug) print("\n\nProcessing results of importing/updating Access export csv files:\n") print('Number of address successfully imported/updated: ', upserted_address_count) print('Initial contact count: ', initial_contact_count, '. final contact count: ', Place.objects.count(), end="\n") @@ -290,16 +292,16 @@ def import_households(households, division1_slug, division2_slug): return successfully_processed_count -def import_attendees(peoples, division3_slug, data_assembly_slug, member_meet_slug, member_character_slug, roaster_meet_slug, data_general_character_slug): +def import_attendees(peoples, division3_slug, data_assembly_slug, member_meet_slug, baptized_meet_slug, roaster_meet_slug, believer_meet_slug): """ Importer of each people from MS Access. :param peoples: file content of people accessible by headers, from MS Access :param division3_slug: key of division 3 # kid :param data_assembly_slug: key of data_assembly :param member_meet_slug: key of member_meet - :param member_character_slug: key of member_character + :param baptized_meet_slug: key of baptized_meet_slug :param roaster_meet_slug: key of roaster_meet_slug - :param data_general_character_slug: key of data_general_character_slug + :param believer_meet_slug: key of believer_meet_slug :return: successfully processed attendee count, also print out importing status and write Photo&FamilyAttendee to Attendees db (create or update) """ gender_converter = { @@ -337,10 +339,16 @@ def import_attendees(peoples, division3_slug, data_assembly_slug, member_meet_sl member_gathering = Gathering.objects.filter(meet=member_meet).last() roaster_meet = Meet.objects.get(slug=roaster_meet_slug) roaster_gathering = Gathering.objects.filter(meet=roaster_meet).last() - pdt = pytz.timezone('America/Los_Angeles') - member_character = Character.objects.get(slug=member_character_slug) - general_character = Character.objects.get(slug=data_general_character_slug) - # attendee_content_type = ContentType.objects.get_for_model(Attendee) + # pdt = pytz.timezone('America/Los_Angeles') + member_character = member_meet.major_character + roaster_character = roaster_meet.major_character + attendee_content_type = ContentType.objects.get_for_model(Attendee) + baptized_meet = Meet.objects.get(slug=baptized_meet_slug) + baptized_category = Category.objects.filter(type='status', display_name='baptized').first() + baptisee_character = baptized_meet.major_character + believer_category = Category.objects.filter(type='status', display_name='receive').first() + believer_meet = Meet.objects.get(slug=believer_meet_slug) + believer_character = believer_meet.major_character successfully_processed_count = 0 # Somehow peoples.line_num incorrect, maybe csv file come with extra new lines. photo_import_results = [] for people in peoples: @@ -376,12 +384,14 @@ def import_attendees(peoples, division3_slug, data_assembly_slug, member_meet_sl 'gender': gender_converter.get(Utility.presence(people.get('Sex', '').upper()), GenderEnum.UNSPECIFIED).name, 'progressions': {attendee_header: Utility.boolean_or_datetext_or_original(people.get(access_header)) for (access_header, attendee_header) in progression_converter.items() if Utility.presence(people.get(access_header)) is not None}, 'infos': { + **Utility.attendee_infos(), 'fixed': { 'access_people_household_id': household_id, 'access_people_values': people, }, 'contacts': contacts, 'names': {}, + 'created_reason': 'CFCCH member/directory registration from importer', } } @@ -411,7 +421,7 @@ def import_attendees(peoples, division3_slug, data_assembly_slug, member_meet_sl ) photo_import_results.append(update_attendee_photo(attendee, Utility.presence(people.get('Photo')))) - update_attendee_membership(pdt, attendee, data_assembly, member_meet, member_character, member_gathering) + update_attendee_membership(baptized_meet, baptized_category, attendee_content_type, attendee, data_assembly, member_meet, member_character, member_gathering, baptisee_character, believer_meet, believer_character, believer_category) if household_role: # filling temporary family roles family = Family.objects.filter(infos__access_household_id=household_id).first() @@ -472,9 +482,9 @@ def import_attendees(peoples, division3_slug, data_assembly_slug, member_meet_sl # } # ) # don't add infos__access_address_id so future query will only get one at family level else: - print("\nCannot find the household id: ", household_id, ' for people: ', people, " Other columns of this people will still be saved. Continuing. \n") + print("\nBad data, cannot find the household id: ", household_id, ' for people: ', people, " Other columns of this people will still be saved. Continuing. \n") - update_attendee_worship_roaster(attendee, data_assembly, visitor_meet, roaster_meet, general_character, roaster_gathering) + update_attendee_worship_roaster(attendee, data_assembly, visitor_meet, roaster_meet, roaster_character, roaster_gathering) else: print('There is no household_id or first/lastname of the people: ', people) successfully_processed_count += 1 @@ -486,7 +496,7 @@ def import_attendees(peoples, division3_slug, data_assembly_slug, member_meet_sl return successfully_processed_count, photo_import_results # list(filter(None.__ne__, photo_import_results)) -def reprocess_directory_emails_and_family_roles(data_assembly_slug, directory_meet_slug, directory_character_slug): +def reprocess_directory_emails_and_family_roles(data_assembly_slug, directory_meet_slug): """ Reprocess extra data (email/relationship) from FamilyAttendee, also do data correction of Role :param data_assembly_slug: key of data_assembly @@ -506,7 +516,7 @@ def reprocess_directory_emails_and_family_roles(data_assembly_slug, directory_me data_assembly = Assembly.objects.get(slug=data_assembly_slug) directory_meet = Meet.objects.get(slug=directory_meet_slug) directory_gathering = Gathering.objects.filter(meet=directory_meet).last() - directory_character = Character.objects.get(slug=directory_character_slug) + directory_character = directory_meet.major_character imported_families = Family.objects.filter(infos__access_household_id__isnull=False).order_by('created') # excludes seed data successfully_processed_count = 0 for family in imported_families: @@ -706,7 +716,7 @@ def update_attendee_worship_roaster(attendee, data_assembly, visitor_meet, roast 'assembly': data_assembly, 'infos': { 'access_household_id': access_household_id, - 'created_reason': 'CFCC member/directory registration from importer', + 'created_reason': 'CFCCH member/directory registration from importer', } } ) @@ -718,7 +728,7 @@ def update_attendee_worship_roaster(attendee, data_assembly, visitor_meet, roast 'registration': data_registration, 'attendee': attendee, 'infos': { - 'created_reason': 'CFCC member/directory registration from importer', + 'created_reason': 'CFCCH member/directory registration from importer', } } ) @@ -728,7 +738,7 @@ def update_attendee_worship_roaster(attendee, data_assembly, visitor_meet, roast meet=visitor_meet, defaults={ 'character': general_character, - 'category': 'primary', + 'category': 'importer', 'start': visitor_meet.start, 'finish': visitor_meet.finish, }, @@ -740,7 +750,7 @@ def update_attendee_worship_roaster(attendee, data_assembly, visitor_meet, roast meet=roaster_meet, defaults={ 'character': general_character, - 'category': 'primary', + 'category': 'importer', 'start': roaster_meet.start, 'finish': datetime.now(pdt) + timedelta(365), # whoever don't attend for a year won't be counted anymore }, @@ -760,13 +770,13 @@ def update_attendee_worship_roaster(attendee, data_assembly, visitor_meet, roast 'finish': roaster_gathering.finish, 'infos': { 'access_household_id': access_household_id, - 'created_reason': 'CFCC member/directory registration from importer', + 'created_reason': 'CFCCH member/directory registration from importer', }, } ) -def update_attendee_membership(pdt, attendee, data_assembly, member_meet, member_character, member_gathering): +def update_attendee_membership(baptized_meet, baptized_category, attendee_content_type, attendee, data_assembly, member_meet, member_character, member_gathering, baptisee_character, believer_meet, believer_character, believer_category): if attendee.progressions.get('cfcc_member'): access_household_id = attendee.infos.get('fixed', {}).get('access_people_household_id') data_registration, data_registration_created = Registration.objects.update_or_create( @@ -777,7 +787,7 @@ def update_attendee_membership(pdt, attendee, data_assembly, member_meet, member 'assembly': data_assembly, 'infos': { 'access_household_id': access_household_id, - 'created_reason': 'CFCC member/directory registration from importer', + 'created_reason': 'CFCCH member/directory registration from importer', } } ) @@ -790,17 +800,18 @@ def update_attendee_membership(pdt, attendee, data_assembly, member_meet, member 'attendee': attendee, 'infos': { 'access_household_id': access_household_id, - 'created_reason': 'CFCC member/directory registration from importer', + 'created_reason': 'CFCCH member/directory registration from importer', } } ) + member_since_or_now = Utility.parsedate_or_now(attendee.progressions.get('member_since')) member_attending_meet_default = { 'attending': data_attending, 'meet': member_meet, 'character': member_character, - 'category': 'active', - 'start': Utility.parsedate_or_now(attendee.progressions.get('member_since')), + 'category': 'active', # member category has to be active or inactive + 'start': member_since_or_now, 'finish': member_meet.finish, } @@ -825,11 +836,64 @@ def update_attendee_membership(pdt, attendee, data_assembly, member_meet, member 'finish': member_gathering.finish, 'infos': { 'access_household_id': access_household_id, - 'created_reason': 'CFCC member/directory registration from importer', + 'created_reason': 'CFCCH member/directory registration from importer', }, } ) + member_since_text = Utility.presence(attendee.progressions.get('member_since')) + member_since_reason = ', member since ' + member_since_text if member_since_text else '' + Past.objects.update_or_create( + organization=data_assembly.division.organization, + content_type=attendee_content_type, + object_id=attendee.id, + category=believer_category, + display_name="會員已信主 member's believer", + when=None, # can't find the exact receive date just by membership + infos={ + **Utility.relationship_infos(), + 'comment': 'CFCCH membership from importer' + member_since_reason, + }, + ) + Past.objects.update_or_create( + organization=data_assembly.division.organization, + content_type=attendee_content_type, + object_id=attendee.id, + category=baptized_category, + display_name="會員已受浸 member's baptized", + when=None, # can't find the exact baptism date just by membership + infos={ + **Utility.relationship_infos(), + 'comment': 'CFCCH membership from importer' + member_since_reason, + }, + ) + + AttendingMeet.objects.update_or_create( + attending=data_attending, + meet=believer_meet, + defaults={ + 'attending': data_attending, + 'meet': believer_meet, + 'character': believer_character, + 'category': 'importer', + 'start': member_since_or_now, + 'finish': believer_meet.finish, + }, + ) + + AttendingMeet.objects.update_or_create( + attending=data_attending, + meet=baptized_meet, + defaults={ + 'attending': data_attending, + 'meet': baptized_meet, + 'character': baptisee_character, + 'category': 'importer', + 'start': member_since_or_now, + 'finish': baptized_meet.finish, + }, + ) + def update_directory_data(data_assembly, family, directory_meet, directory_character, directory_gathering): """ @@ -854,7 +918,7 @@ def update_directory_data(data_assembly, family, directory_meet, directory_chara 'assembly': data_assembly, 'infos': { 'access_household_id': access_household_id, - 'created_reason': 'CFCC member/directory registration from importer', + 'created_reason': 'CFCCH member/directory registration from importer', } } ) @@ -868,7 +932,7 @@ def update_directory_data(data_assembly, family, directory_meet, directory_chara 'attendee': family_member, 'infos': { 'access_household_id': access_household_id, - 'created_reason': 'CFCC member/directory registration from importer', + 'created_reason': 'CFCCH member/directory registration from importer', } } ) @@ -880,7 +944,7 @@ def update_directory_data(data_assembly, family, directory_meet, directory_chara 'attending': directory_attending, 'meet': directory_meet, 'character': directory_character, - 'category': 'secondary', + 'category': 'importer', 'start': directory_gathering.start, 'finish': directory_gathering.finish, } @@ -900,7 +964,7 @@ def update_directory_data(data_assembly, family, directory_meet, directory_chara 'finish': directory_gathering.finish, 'infos': { 'access_household_id': access_household_id, - 'created_reason': 'CFCC member/directory registration from importer', + 'created_reason': 'CFCCH member/directory registration from importer', }, } ) @@ -946,7 +1010,7 @@ def update_attendee_photo(attendee, photo_names): def return_two_phones(phones): - cleaned_phones = list(set([re.sub("[^0-9\+]+", "", p) for p in phones if (p and not p.isspace())])) + cleaned_phones = list(set([re.sub("[^0-9\+()-]+", "", p) for p in phones if (p and not p.isspace())])) return (cleaned_phones + [None, None])[0:2] @@ -964,6 +1028,9 @@ def save_two_phones(attendee, phone): def add_int_code(phone, default='+1'): if phone and not phone.isspace(): + if '-' not in phone and len(phone) == 10: + phone = f'({phone[0:3]}){phone[3:6]}-{phone[6:10]}' + if '+' in phone: return phone else: @@ -971,6 +1038,7 @@ def add_int_code(phone, default='+1'): else: return None + def check_all_headers(): #households_headers = ['HouseholdID', 'HousholdLN', 'HousholdFN', 'SpouseFN', 'AddressID', 'HouseholdPhone', 'HouseholdFax', 'AttendenceCount', 'FlyerMailing', 'CardMailing', 'UpdateDir', 'PrintDir', 'LastUpdate', 'HouseholdNote', 'FirstDate', '海沃之友', 'Congregation'] #peoples_headers = ['LastName', 'FirstName', 'NickName', 'ChineseName', 'Photo', 'Sex', 'Active', 'HouseholdID', 'HouseholdRole', 'E-mail', 'WorkPhone', 'WorkExtension', 'CellPhone', 'BirthDate', 'Skills', 'FirstDate', 'BapDate', 'BapLocation', 'Member', 'MemberDate', 'Fellowship', 'Group', 'LastContacted', 'AssignmentID', 'LastUpdated', 'PeopleNote', 'Christian'] @@ -988,10 +1056,13 @@ def run( data_assembly_slug, member_meet_slug, directory_meet_slug, - member_character_slug, - directory_character_slug, + baptized_meet_slug, + # member_character_slug, + # directory_character_slug, roaster_meet_slug, - data_general_character_slug, + # data_general_character_slug, + # data_baptisee_character_slug, + believer_meet_slug, *extras ): """ @@ -1005,10 +1076,9 @@ def run( :param data_assembly_slug: key of data_assembly :param member_meet_slug: key of member_meet :param directory_meet_slug: key of directory_meet - :param member_character_slug: key of member_character - :param directory_character_slug: key of directory_character + :param baptized_meet_slug: key of baptized_meet :param roaster_meet_slug: key of roaster_meet_slug - :param data_general_character_slug: key of data_general_character_slug + :param believer_meet_slug: key of believer_meet_slug :param extras: optional other arguments :return: None, but write to Attendees db (create or update) """ @@ -1023,10 +1093,10 @@ def run( print("Reading data_assembly_slug: ", data_assembly_slug) print("Reading member_meet_slug: ", member_meet_slug) print("Reading directory_meet_slug: ", directory_meet_slug) - print("Reading member_character_slug: ", member_character_slug) - print("Reading directory_character_slug: ", directory_character_slug) + # print("Reading member_character_slug: ", member_character_slug) + # print("Reading directory_character_slug: ", directory_character_slug) print("Reading roaster_meet_slug: ", roaster_meet_slug) - print("Reading data_general_character_slug: ", data_general_character_slug) + print("Reading believer_meet_slug: ", believer_meet_slug) print("Reading extras: ", extras) print("Divisions required for importing, running commands: docker-compose -f local.yml run django python manage.py runscript load_access_csv --script-args path/tp/household.csv path/to/people.csv path/to/address.csv division1_slug division2_slug division3_slug member_data_assembly_member_meet_slug directory_meet_slug member_character_slug directory_character_slug") @@ -1042,8 +1112,11 @@ def run( data_assembly_slug, member_meet_slug, directory_meet_slug, - member_character_slug, - directory_character_slug, + baptized_meet_slug, + # member_character_slug, + # directory_character_slug, roaster_meet_slug, - data_general_character_slug, + believer_meet_slug, + # data_general_character_slug, + # data_baptisee_character_slug, ) diff --git a/attendees/static/js/persons/attendee_update_view.js b/attendees/static/js/persons/attendee_update_view.js index 04e462cc..9ed8cc42 100644 --- a/attendees/static/js/persons/attendee_update_view.js +++ b/attendees/static/js/persons/attendee_update_view.js @@ -48,6 +48,7 @@ Attendees.datagridUpdate = { division: 0, // will be assigned later display_name: '', // will be assigned later }, + meetCharacters: null, init: () => { console.log('/static/js/persons/attendee_update_view.js'); @@ -61,6 +62,23 @@ Attendees.datagridUpdate = { $("div.form-container").on('click', 'button.place-button', e => Attendees.datagridUpdate.initPlacePopupDxForm(e)); $("div.form-container").on('click', 'button.family-button', e => Attendees.datagridUpdate.initFamilyAttrPopupDxForm(e)); Attendees.datagridUpdate.attachContactAddButton(); + + $(window).keydown((event) => { + if (event.keyCode === 13) { + DevExpress.ui.notify( + { + message: 'Click the "Save Attendee" button to save data', + width: 500, + position: { + my: 'center', + at: 'center', + of: window, + }, + }, 'warning', 1000); + event.preventDefault(); + return false; + } // prevent user to submit form by hitting enter + }); // add listeners for Family, counselling, etc. }, @@ -179,7 +197,7 @@ Attendees.datagridUpdate = { elementAttr: { class: 'attendee-form-submits', // for toggling editing mode }, - text: 'Add more contact', + text: 'Add more contacts', icon: 'email', // or 'fas fa-comment-dots' stylingMode: 'outlined', type: 'success', @@ -566,122 +584,143 @@ Attendees.datagridUpdate = { const buttonItems = [ { - itemType: 'button', - name: 'mainAttendeeFormSubmit', - horizontalAlignment: 'left', - buttonOptions: { - elementAttr: { - class: 'attendee-form-submits', // for toggling editing mode - }, - disabled: !Attendees.utilities.editingEnabled, - text: 'Save Attendee details and photo', - icon: 'save', - hint: 'save attendee data in the page', - type: 'default', - useSubmitBehavior: true, - onClick: (e) => { - if (Attendees.datagridUpdate.attendeeMainDxForm.validate().isValid && confirm('Are you sure?')) { - - const userData = new FormData($('form#attendee-update-form')[0]); - if (!$('input[name="photo"]')[0].value) { - userData.delete('photo') - } - const userInfos = Attendees.datagridUpdate.attendeeFormConfigs.formData.infos; - userInfos['contacts'] = Attendees.utilities.trimBothKeyAndValueButKeepBasicContacts(userInfos.contacts); // remove emptied contacts - userData.set('infos', JSON.stringify(userInfos)); - - $.ajax({ - url: Attendees.datagridUpdate.attendeeAjaxUrl, - contentType: false, - processData: false, - dataType: 'json', - data: userData, - method: Attendees.datagridUpdate.attendeeId && Attendees.datagridUpdate.attendeeId !== 'new' ? 'PUT' : 'POST', - success: (response) => { // Todo: update photo link, temporarily reload to bypass the requirement - const parser = new URL(window.location); - parser.searchParams.set('success', 'Saving attendee success'); - - if (parser.href.split('/').pop().startsWith('new')){ - const newAttendeeIdUrl = '/' + response.id; - window.location = parser.href.replace('/new', newAttendeeIdUrl); - }else { - window.location = parser.href; + colSpan: 24, + colCount: 24, + cssClass: 'h6 not-shrinkable', + itemType: 'group', + items: [ + { + itemType: 'button', + name: 'mainAttendeeFormSubmit', + horizontalAlignment: 'left', + buttonOptions: { + elementAttr: { + class: 'attendee-form-submits', // for toggling editing mode + }, + disabled: !Attendees.utilities.editingEnabled, + text: 'Save Attendee details and photo', + icon: 'save', + hint: 'save attendee data in the page', + type: 'default', + useSubmitBehavior: false, + onClick: (e) => { + const validationResults = Attendees.datagridUpdate.attendeeMainDxForm.validate(); + if (validationResults.isValid && confirm('Are you sure?')) { + + const userData = new FormData($('form#attendee-update-form')[0]); + if (!$('input[name="photo"]')[0].value) { + userData.delete('photo') } - }, - error: (response) => { - console.log('Failed to save data for main AttendeeForm, error: ', response); - console.log('formData: ', [...userData]); + const userInfos = Attendees.datagridUpdate.attendeeFormConfigs.formData.infos; + userInfos['contacts'] = Attendees.utilities.trimBothKeyAndValueButKeepBasicContacts(userInfos.contacts); // remove emptied contacts + userData.set('infos', JSON.stringify(userInfos)); + + $.ajax({ + url: Attendees.datagridUpdate.attendeeAjaxUrl, + contentType: false, + processData: false, + dataType: 'json', + data: userData, + method: Attendees.datagridUpdate.attendeeId && Attendees.datagridUpdate.attendeeId !== 'new' ? 'PUT' : 'POST', + success: (response) => { // Todo: update photo link, temporarily reload to bypass the requirement + const parser = new URL(window.location); + parser.searchParams.set('success', 'Saving attendee success'); + + if (parser.href.split('/').pop().startsWith('new')) { + const newAttendeeIdUrl = '/' + response.id; + window.location = parser.href.replace('/new', newAttendeeIdUrl); + } else { + window.location = parser.href; + } + }, + error: (response) => { + console.log('Failed to save data for main AttendeeForm, error: ', response); + console.log('formData: ', [...userData]); + DevExpress.ui.notify( + { + message: 'saving attendee error', + width: 500, + position: { + my: 'center', + at: 'center', + of: window, + }, + }, 'error', 5000); + }, + }); + } else if (!validationResults.isValid) { + validationMessages = validationResults.brokenRules.reduce((all, now) => {all.push(now.message); return all}, []); DevExpress.ui.notify( { - message: 'saving attendee error', + message: validationMessages.join('. '), width: 500, position: { my: 'center', at: 'center', of: window, }, - }, 'error', 5000); - }, - }); - } - } - }, - }, - { - itemType: 'button', - name: 'mainAttendeeFormDelete', - horizontalAlignment: 'left', - buttonOptions: { - elementAttr: { - class: 'attendee-form-delete', // for toggling editing mode + }, 'error', 2000); + } + } + }, }, - disabled: !Attendees.utilities.editingEnabled, - text: "Delete all of Attendee's data", - icon: 'trash', - hint: "delete attendee's all data in the page", - type: 'danger', - onClick: (e) => { - if (confirm('Are you sure to delete all data of the attendee? Everything of the attendee will be removed. Instead, seetting finish/deathday is usually enough!')) { - window.scrollTo(0,0); - $('div.spinner-border').show(); - $.ajax({ - url: Attendees.datagridUpdate.attendeeAjaxUrl, - method: 'DELETE', - success: (response) => { - $('div.spinner-border').hide(); - DevExpress.ui.notify( - { - message: 'delete attendee success', - width: 500, - position: { - my: 'center', - at: 'center', - of: window, - }, - }, 'info', 2500); - window.location = new URL(window.location.origin); - }, - error: (response) => { - console.log('Failed to delete data for main AttendeeForm, error: ', response); - DevExpress.ui.notify( - { - message: 'saving attendee error', - width: 500, - position: { - my: 'center', - at: 'center', - of: window, - }, - }, 'error', 5000); - }, - }); - } - } - }, + { + itemType: 'button', + name: 'mainAttendeeFormDelete', + horizontalAlignment: 'left', + buttonOptions: { + elementAttr: { + class: 'attendee-form-delete', // for toggling editing mode + }, + disabled: !Attendees.utilities.editingEnabled, + text: "Delete all of Attendee's data", + icon: 'trash', + hint: "delete attendee's all data in the page", + type: 'danger', + onClick: (e) => { + if (confirm('Are you sure to delete all data of the attendee? Everything of the attendee will be removed. Instead, seetting finish/deathday is usually enough!')) { + window.scrollTo(0, 0); + $('div.spinner-border').show(); + $.ajax({ + url: Attendees.datagridUpdate.attendeeAjaxUrl, + method: 'DELETE', + success: (response) => { + $('div.spinner-border').hide(); + DevExpress.ui.notify( + { + message: 'delete attendee success', + width: 500, + position: { + my: 'center', + at: 'center', + of: window, + }, + }, 'info', 2500); + window.location = new URL(window.location.origin); + }, + error: (response) => { + console.log('Failed to delete data for main AttendeeForm, error: ', response); + DevExpress.ui.notify( + { + message: 'saving attendee error', + width: 500, + position: { + my: 'center', + at: 'center', + of: window, + }, + }, 'error', 5000); + }, + }); + } + } + }, + }, + ], }, ]; - const originalItems = [...basicItems, ...(Attendees.datagridUpdate.attendeeId === 'new' ? [] : moreItems ), ...buttonItems]; + const originalItems = [...basicItems, ...buttonItems, ...(Attendees.datagridUpdate.attendeeId === 'new' ? [] : moreItems )]; return { showValidationSummary: true, @@ -868,11 +907,13 @@ Attendees.datagridUpdate = { { colSpan: 7, dataField: 'actual_birthday', + helpText: 'month / day / year', editorType: 'dxDateBox', label: { text: 'Real birthday', }, editorOptions: { + showClearButton: true, placeholder: 'click calendar', elementAttr: { title: 'month, day and year are all required', @@ -903,17 +944,39 @@ Attendees.datagridUpdate = { }, { colSpan: 7, - dataField: 'infos.contacts.phone1', + dataField: 'infos.contacts.phone1', // DxTextBox maskRules can't accept variable length of country codes + helpText: 'format: +1(510)123-4567', label: { text: 'phone1', }, + editorOptions: { + placeholder: '+1(000)000-0000', + }, + validationRules: [ + { + type: 'pattern', + pattern: /^(\+\d{1,3})(\(\d{1,3}\))([0-9a-zA-Z]{2,6})-([,0-9a-zA-Z]{3,10})$/, + message: "Must be '+' national&area code like +1(510)123-4567,890 Comma for extension", + }, + ], }, { colSpan: 7, dataField: 'infos.contacts.phone2', + helpText: 'ie. +1(000)000-0000', label: { text: 'phone2', }, + editorOptions: { + placeholder: '+1(000)000-0000', + }, + validationRules: [ + { + type: 'pattern', + pattern: /^(\+\d{1,3})(\(\d{1,3}\))([0-9a-zA-Z]{2,6})-([,0-9a-zA-Z]{3,10})$/, + message: "Must be '+' national&area code like +1(510)123-4567,890 Comma for extension", + }, + ], }, { colSpan: 7, @@ -928,6 +991,12 @@ Attendees.datagridUpdate = { label: { text: 'email1', }, + validationRules: [ + { + type: "email", + message: "Email is invalid" + }, + ], }, { colSpan: 7, @@ -935,6 +1004,12 @@ Attendees.datagridUpdate = { label: { text: 'email2', }, + validationRules: [ + { + type: "email", + message: "Email is invalid" + }, + ], }, { colSpan: 7, @@ -2968,6 +3043,9 @@ Attendees.datagridUpdate = { of: window, }, }, 'success', 2000); + if (args.type === 'status') { + Attendees.datagridUpdate.attendingMeetDatagrid.refresh(); + } }, }); }, @@ -3148,6 +3226,7 @@ Attendees.datagridUpdate = { of: window, }, }, 'success', 2000); + Attendees.datagridUpdate.statusDatagrid.refresh(); }, }); }, @@ -3225,10 +3304,10 @@ Attendees.datagridUpdate = { groupIndex: 0, validationRules: [{type: 'required'}], caption: 'Group (Assembly)', - setCellValue: (rowData, value) => { - rowData.assembly = value; - rowData.meet = null; - rowData.character = null; + setCellValue: (newData, value, currentData) => { + newData.assembly = value; + newData.meet = null; + newData.character = null; }, lookup: { valueExpr: 'id', @@ -3254,6 +3333,11 @@ Attendees.datagridUpdate = { dataField: 'meet', caption: 'Activity (Meet)', validationRules: [{type: 'required'}], + setCellValue: (newData, value, currentData) => { + newData.meet = value; + const majorCharacter = Attendees.datagridUpdate.meetCharacters[value]; + if (majorCharacter && currentData.character === null) {newData.character = majorCharacter;} + }, // setting majorCharacter of the corresponding meet lookup: { valueExpr: 'id', displayExpr: 'display_name', @@ -3263,7 +3347,15 @@ Attendees.datagridUpdate = { store: new DevExpress.data.CustomStore({ key: 'id', load: (searchOpts) => { - return $.getJSON(Attendees.datagridUpdate.attendeeAttrs.dataset.meetsEndpoint, searchOpts.filter); + const d = new $.Deferred(); + $.getJSON(Attendees.datagridUpdate.attendeeAttrs.dataset.meetsEndpoint, searchOpts.filter) + .done((result) => { + if (Attendees.datagridUpdate.meetCharacters === null) { + Attendees.datagridUpdate.meetCharacters = result.data.reduce((all, now)=> {all[now.id] = now.major_character; return all}, {}); + } // cache the every meet's major characters for later use + d.resolve(result); + }); + return d.promise(); }, byKey: (key) => { const d = new $.Deferred(); diff --git a/attendees/static/js/persons/attendees_list_view.js b/attendees/static/js/persons/attendees_list_view.js index d73e43d2..7d3c5d3d 100644 --- a/attendees/static/js/persons/attendees_list_view.js +++ b/attendees/static/js/persons/attendees_list_view.js @@ -136,11 +136,6 @@ Attendees.dataAttendees = { }, initialAttendeesColumns: [ -// { -// caption: "attendee_id", -// dataField: "id", -// dataType: "string", -// }, { caption: "Full name", // allowSorting: false, @@ -157,6 +152,22 @@ Attendees.dataAttendees = { $($('', attrs)).appendTo(container); }, }, + { + dataField: "first_name", + visible: false, + }, + { + dataField: "last_name", + visible: false, + }, + { + dataField: "last_name2", + visible: false, + }, + { + dataField: "first_name2", + visible: false, + }, { dataHtmlTitle: "showing only divisions of current user organization", caption: "division", diff --git a/attendees/static/js/shared/utilities.js b/attendees/static/js/shared/utilities.js index 202452cf..1f894afc 100644 --- a/attendees/static/js/shared/utilities.js +++ b/attendees/static/js/shared/utilities.js @@ -38,7 +38,7 @@ Attendees.utilities = { }, toggleDxFormGroups: (animationSpeed="fast") => { - $(".dx-form-group-caption") + $(".h6:not(.not-shrinkable) .dx-form-group-caption") .each(function () { $(this) .prepend( diff --git a/attendees/templates/persons/attendee_update_view.html b/attendees/templates/persons/attendee_update_view.html index 82975f78..27b39414 100644 --- a/attendees/templates/persons/attendee_update_view.html +++ b/attendees/templates/persons/attendee_update_view.html @@ -69,7 +69,7 @@

+ method="POST"> diff --git a/attendees/users/models/user.py b/attendees/users/models/user.py index 0ae5690b..d2093a5f 100644 --- a/attendees/users/models/user.py +++ b/attendees/users/models/user.py @@ -72,7 +72,7 @@ def is_counselor(self): return self.belongs_to_groups_of(organization_counselor_groups) def attendee_uuid_str(self): - return str(self.attendee.id) if self.attendee else '' + return str(self.attendee.id) if hasattr(self, 'attendee') else '' def attend_divisions_of(self, division_slugs): return self.attendee and self.attendee.attending_set.filter(divisions__slug__in=division_slugs).exists() diff --git a/fixtures/db_seed.json b/fixtures/db_seed.json index 22545d93..8f34799c 100644 --- a/fixtures/db_seed.json +++ b/fixtures/db_seed.json @@ -1163,7 +1163,7 @@ "category": "main", "parent": 12, "html_type": "a", - "urn": "/persons/cfcch_data_management/cfcch_congregation_data/datagrid_assembly_data_attendings/?characters=d7c8Fd_cfcch_congregation_data_general", + "urn": "/persons/cfcch_data_management/cfcch_congregation_data/datagrid_assembly_data_attendings/?characters=d7c8Fd_cfcch_congregation_data_roaster", "url_name": "datagrid_assembly_data_attendings", "display_name": "\u6703\u773e\u53c3\u8207 attending", "display_order": 3200, @@ -1495,9 +1495,11 @@ "slug": "a8dE2e_none", "display_name": "No organization assigned", "infos": { - "flags": { + "settings": { "opencc_convert": true, - "attendance_character_to_past_categories": {} + "attendee_to_attending": true, + "attendingmeet_meet_to_past_category": {}, + "past_category_to_attendingmeet_meet": {} }, "acronym": null, "contacts": {}, @@ -1518,10 +1520,16 @@ "slug": "d7c8Fd_cfcc_hayward", "display_name": "CFCCH", "infos": { - "flags": { + "settings": { "opencc_convert": true, - "attendance_character_to_past_categories": { - "baptisee": 5 + "attendee_to_attending": true, + "attendingmeet_meet_to_past_category": { + "17": 4, + "16": 5 + }, + "past_category_to_attendingmeet_meet": { + "4": 17, + "5": 16 } }, "acronym": "CFCCH", @@ -1559,9 +1567,11 @@ "slug": "faBd6C_heaven", "display_name": "\u5929\u5802 Heaven", "infos": { - "flags": { + "settings": { "opencc_convert": true, - "attendance_character_to_past_categories": {} + "attendee_to_attending": true, + "attendingmeet_meet_to_past_category": {}, + "past_category_to_attendingmeet_meet": {} }, "acronym": "Kingdom", "contacts": {}, @@ -2297,11 +2307,11 @@ "pk": 3, "fields": { "created": "2021-05-23T12:55:51.864Z", - "modified": "2021-05-23T12:59:01.791Z", + "modified": "2021-10-22T15:12:40.352Z", "is_removed": false, "type": "status", "display_order": 0, - "display_name": "believe", + "display_name": "general", "infos": {} } }, @@ -2463,6 +2473,45 @@ "infos": {} } }, + { + "model": "persons.category", + "pk": 16, + "fields": { + "created": "2021-10-22T15:14:47.127Z", + "modified": "2021-10-22T15:14:47.127Z", + "is_removed": false, + "type": "education", + "display_order": 0, + "display_name": "primary", + "infos": {} + } + }, + { + "model": "persons.category", + "pk": 17, + "fields": { + "created": "2021-10-22T15:15:04.108Z", + "modified": "2021-10-22T15:15:04.108Z", + "is_removed": false, + "type": "education", + "display_order": 0, + "display_name": "secondary", + "infos": {} + } + }, + { + "model": "persons.category", + "pk": 18, + "fields": { + "created": "2021-10-22T15:15:15.508Z", + "modified": "2021-10-22T15:15:15.508Z", + "is_removed": false, + "type": "education", + "display_order": 0, + "display_name": "alternative", + "infos": {} + } + }, { "model": "persons.past", "pk": "070ade81-dbf4-47c4-a1e0-046f553021b8", @@ -5910,7 +5959,7 @@ "assembly": 5, "display_name": "\u5e38\u505a\u79ae\u62dc\u6703\u773e", "display_order": 0, - "slug": "d7c8Fd_cfcch_congregation_data_general", + "slug": "d7c8Fd_cfcch_congregation_data_roaster", "info": null, "type": "audience" } @@ -6010,10 +6059,10 @@ "pk": 22, "fields": { "created": "2020-06-19T21:58:12.422Z", - "modified": "2021-04-08T16:46:32.730Z", + "modified": "2021-10-22T15:08:18.181Z", "is_removed": false, "assembly": 5, - "display_name": "\u5165\u7c4d\u6703\u54e1", + "display_name": "\u5165\u7c4d\u6703\u54e1 member", "display_order": 0, "slug": "d7c8Fd_cfcch_congregation_data_member", "info": null, @@ -6025,16 +6074,61 @@ "pk": 23, "fields": { "created": "2020-06-19T21:58:12.422Z", - "modified": "2021-04-08T17:47:00.152Z", + "modified": "2021-10-22T15:08:34.366Z", "is_removed": false, "assembly": 5, - "display_name": "\u5217\u5165\u901a\u8a0a\u9304", + "display_name": "\u5217\u5165\u901a\u8a0a\u9304 in directory", "display_order": 0, "slug": "d7c8Fd_cfcch_congregation_data_directory", "info": null, "type": "internal" } }, + { + "model": "occasions.character", + "pk": 24, + "fields": { + "created": "2020-10-09T21:58:12.422Z", + "modified": "2021-10-08T16:46:32.730Z", + "is_removed": false, + "assembly": 5, + "display_name": "\u5df2\u53d7\u6d78 baptisee", + "display_order": 0, + "slug": "d7c8Fd_cfcch_congregation_data_baptisee", + "info": null, + "type": "internal" + } + }, + { + "model": "occasions.character", + "pk": 25, + "fields": { + "created": "2021-10-22T15:09:58.833Z", + "modified": "2021-10-22T15:18:05.224Z", + "is_removed": false, + "assembly": 5, + "display_name": "\u5df2\u4fe1\u4e3b believer", + "display_order": 0, + "slug": "d7c8Fd_cfcch_congregation_data_believer", + "info": null, + "type": "internal" + } + }, + { + "model": "occasions.character", + "pk": 26, + "fields": { + "created": "2021-10-22T15:09:58.833Z", + "modified": "2021-10-22T15:18:05.224Z", + "is_removed": false, + "assembly": 5, + "display_name": "\u8a2a\u5ba2 visitor", + "display_order": 0, + "slug": "d7c8Fd_cfcch_congregation_data_visitor", + "info": null, + "type": "internal" + } + }, { "model": "occasions.meet", "pk": -1, @@ -6043,6 +6137,7 @@ "modified": "2021-04-08T14:03:35.930Z", "is_removed": false, "assembly": 5, + "major_character": null, "shown_audience": false, "audience_editable": false, "start": "2020-05-12T14:53:44Z", @@ -6065,6 +6160,7 @@ "modified": "2021-04-08T14:03:55.729Z", "is_removed": false, "assembly": 5, + "major_character": 25, "shown_audience": false, "audience_editable": false, "start": "2020-05-12T14:53:44Z", @@ -6087,6 +6183,7 @@ "modified": "2021-04-08T16:37:22.469Z", "is_removed": false, "assembly": 1, + "major_character": 1, "shown_audience": true, "audience_editable": true, "start": "2020-05-12T14:53:44Z", @@ -6108,6 +6205,7 @@ "modified": "2021-04-08T16:37:34.616Z", "is_removed": false, "assembly": 1, + "major_character": 1, "shown_audience": true, "audience_editable": true, "start": "2020-05-12T14:53:56Z", @@ -6129,6 +6227,7 @@ "modified": "2021-04-08T14:04:29.830Z", "is_removed": false, "assembly": 2, + "major_character": 6, "shown_audience": true, "audience_editable": true, "start": "2020-05-12T14:55:20Z", @@ -6150,6 +6249,7 @@ "modified": "2021-04-08T16:40:16.599Z", "is_removed": false, "assembly": 4, + "major_character": 7, "shown_audience": true, "audience_editable": true, "start": "2020-05-12T14:53:19Z", @@ -6171,6 +6271,7 @@ "modified": "2021-04-08T16:37:02.414Z", "is_removed": false, "assembly": 1, + "major_character": null, "shown_audience": true, "audience_editable": true, "start": "2020-05-12T14:54:57Z", @@ -6192,6 +6293,7 @@ "modified": "2021-04-08T16:36:49.060Z", "is_removed": false, "assembly": 3, + "major_character": null, "shown_audience": true, "audience_editable": true, "start": "2020-05-12T14:53:03Z", @@ -6210,14 +6312,15 @@ "pk": 7, "fields": { "created": "2020-05-09T22:34:04.419Z", - "modified": "2021-04-08T14:01:38.639Z", + "modified": "2021-10-23T18:06:43.299Z", "is_removed": false, "assembly": 5, + "major_character": 15, "shown_audience": true, "audience_editable": false, "start": "2020-05-12T14:54:44Z", - "finish": "2099-05-12T15:54:46Z", - "display_name": "\u4e3b\u65e5\u5d07\u62dc roaster", + "finish": "2200-05-12T15:54:46Z", + "display_name": "\u5e38\u505a\u79ae\u62dc Worship roaster", "slug": "d7c8Fd_cfcch_congregation_roaster", "infos": { "default_time_zone": "America/Los_Angeles", @@ -6235,6 +6338,7 @@ "modified": "2021-04-08T16:13:39.869Z", "is_removed": false, "assembly": 5, + "major_character": 23, "shown_audience": false, "audience_editable": false, "start": "2020-05-12T14:54:35Z", @@ -6256,6 +6360,7 @@ "modified": "2021-04-08T16:14:05.544Z", "is_removed": false, "assembly": 5, + "major_character": 22, "shown_audience": false, "audience_editable": false, "start": "2020-05-12T14:54:24Z", @@ -6277,6 +6382,7 @@ "modified": "2021-04-08T16:32:15.494Z", "is_removed": false, "assembly": 6, + "major_character": null, "shown_audience": true, "audience_editable": false, "start": "2020-07-17T15:28:40Z", @@ -6298,6 +6404,7 @@ "modified": "2021-04-08T16:34:17.114Z", "is_removed": false, "assembly": 6, + "major_character": null, "shown_audience": true, "audience_editable": false, "start": "2020-07-17T15:30:54Z", @@ -6319,6 +6426,7 @@ "modified": "2021-04-08T16:34:34.804Z", "is_removed": false, "assembly": 6, + "major_character": null, "shown_audience": true, "audience_editable": false, "start": "2020-07-17T15:34:41Z", @@ -6340,6 +6448,7 @@ "modified": "2021-04-08T16:34:54.420Z", "is_removed": false, "assembly": 6, + "major_character": null, "shown_audience": true, "audience_editable": false, "start": "2020-07-17T16:28:13Z", @@ -6361,6 +6470,7 @@ "modified": "2021-04-08T16:35:15.657Z", "is_removed": false, "assembly": 6, + "major_character": null, "shown_audience": true, "audience_editable": false, "start": "2020-07-17T16:32:20Z", @@ -6382,6 +6492,7 @@ "modified": "2021-04-08T16:35:33.036Z", "is_removed": false, "assembly": 6, + "major_character": null, "shown_audience": true, "audience_editable": false, "start": "2020-07-17T16:38:38Z", @@ -6395,6 +6506,50 @@ "site_id": "2" } }, + { + "model": "occasions.meet", + "pk": 16, + "fields": { + "created": "2020-10-09T22:34:04.419Z", + "modified": "2021-10-23T18:08:26.928Z", + "is_removed": false, + "assembly": 5, + "major_character": 24, + "shown_audience": true, + "audience_editable": false, + "start": "2020-05-12T14:54:44Z", + "finish": "2200-05-12T15:54:46Z", + "display_name": "\u5df2\u53d7\u6d17 baptized", + "slug": "d7c8Fd_cfcch_congregation_baptized", + "infos": { + "default_time_zone": "America/Los_Angeles" + }, + "site_type": 35, + "site_id": "1" + } + }, + { + "model": "occasions.meet", + "pk": 17, + "fields": { + "created": "2020-10-09T22:34:04.419Z", + "modified": "2021-10-23T18:08:14.380Z", + "is_removed": false, + "assembly": 5, + "major_character": 25, + "shown_audience": true, + "audience_editable": false, + "start": "2020-05-12T14:54:44Z", + "finish": "2200-05-12T15:54:46Z", + "display_name": "\u5df2\u4fe1\u4e3b believer", + "slug": "d7c8Fd_cfcch_congregation_believer", + "infos": { + "default_time_zone": "America/Los_Angeles" + }, + "site_type": 35, + "site_id": "1" + } + }, { "model": "occasions.gathering", "pk": 1, @@ -9256,7 +9411,7 @@ "modified": "2021-04-08T13:55:47.966Z", "is_removed": false, "organization": 1, - "display_name": "\u672a\u6307\u5b9aunspecified", + "display_name": "\u672a\u6307\u5b9a CFCCH unspecified", "slug": "cfcch_unspecified", "audience_auth_group": 0 } @@ -9344,14 +9499,14 @@ "pk": "0498c414-abd3-4173-add1-5e42053760e4", "fields": { "created": "1999-04-11T19:36:20.422Z", - "modified": "2020-11-16T16:06:28.012Z", + "modified": "2021-10-22T15:24:30.318Z", "is_removed": false, "division": 2, "user": null, "first_name": "David", - "last_name": "\u5927\u885b", - "first_name2": null, - "last_name2": null, + "last_name": "", + "first_name2": "\u5927\u885b", + "last_name2": "", "gender": "MALE", "actual_birthday": null, "estimated_birthday": null, @@ -9361,14 +9516,12 @@ "infos": { "fixed": {}, "names": { - "original": "David \u5927\u885b" + "original": "David \u5927\u885b", + "simplified": "David \u5927\u536b", + "traditional": "David \u5927\u885b", + "romanization": "David Da Wei " }, - "contacts": { - "email1": null, - "email2": null, - "phone1": null, - "phone2": null - } + "contacts": {} } } }, @@ -9377,14 +9530,14 @@ "pk": "22c29cd5-a5ab-46a3-afe6-d5a600c33fc9", "fields": { "created": "1999-04-11T19:32:19.822Z", - "modified": "2020-11-16T16:04:00.996Z", + "modified": "2021-10-22T15:24:47.679Z", "is_removed": false, "division": 2, "user": 2, - "first_name": "\u590f\u7532", - "last_name": "Hagar", - "first_name2": null, - "last_name2": null, + "first_name": "Hagar", + "last_name": "", + "first_name2": "\u590f\u7532", + "last_name2": "", "gender": "FEMALE", "actual_birthday": null, "estimated_birthday": null, @@ -9396,14 +9549,12 @@ "infos": { "fixed": {}, "names": { - "original": "\u590f\u7532 Hagar" + "original": "Hagar \u590f\u7532", + "simplified": "Hagar \u590f\u7532", + "traditional": "Hagar \u590f\u7532", + "romanization": "Hagar Xia Jia " }, - "contacts": { - "email1": null, - "email2": null, - "phone1": null, - "phone2": null - } + "contacts": {} } } }, @@ -9412,14 +9563,14 @@ "pk": "3566e642-3624-42e3-9644-c54b08d8c6d6", "fields": { "created": "1999-04-11T19:37:15.410Z", - "modified": "2020-11-16T16:12:08.159Z", + "modified": "2021-10-22T15:25:07.861Z", "is_removed": false, "division": 1, "user": null, - "first_name": "\u53c3\u5b6b", - "last_name": "Samson", + "first_name": "Samson", + "last_name": "", "first_name2": "\u53c3\u5b6b", - "last_name2": null, + "last_name2": "", "gender": "MALE", "actual_birthday": null, "estimated_birthday": null, @@ -9430,14 +9581,12 @@ "fixed": {}, "names": { "nick": "\u5f1f\u5144", - "original": "\u53c3\u5b6b Samson \u53c3\u5b6b" + "original": "Samson \u53c3\u5b6b", + "simplified": "Samson \u53c2\u5b59", + "traditional": "Samson \u53c3\u5b6b", + "romanization": "Samson Can Sun " }, - "contacts": { - "email1": null, - "email2": null, - "phone1": null, - "phone2": null - } + "contacts": {} } } }, @@ -9481,14 +9630,14 @@ "pk": "3833d04e-0897-45da-abf6-7d750f0baee7", "fields": { "created": "1999-04-11T19:34:37.600Z", - "modified": "2020-11-16T16:06:33.797Z", + "modified": "2021-10-22T15:25:28.948Z", "is_removed": false, "division": 2, "user": null, - "first_name": "\u4e9e\u6bd4\u8a72", - "last_name": "Abigail", + "first_name": "Abigail", + "last_name": "", "first_name2": "\u4e9e\u6bd4\u8a72", - "last_name2": null, + "last_name2": "", "gender": "FEMALE", "actual_birthday": null, "estimated_birthday": null, @@ -9499,14 +9648,12 @@ "fixed": {}, "names": { "nick": "\u8001\u59ca", - "original": "\u4e9e\u6bd4\u8a72 Abigail \u4e9e\u6bd4\u8a72" + "original": "Abigail \u4e9e\u6bd4\u8a72", + "simplified": "Abigail \u4e9a\u6bd4\u8be5", + "traditional": "Abigail \u4e9e\u6bd4\u8a72", + "romanization": "Abigail Ya Bi Gai " }, - "contacts": { - "email1": null, - "email2": null, - "phone1": null, - "phone2": null - } + "contacts": {} } } }, @@ -9515,14 +9662,14 @@ "pk": "44e894a1-1190-4916-992a-b5eb41b35b16", "fields": { "created": "1999-04-11T19:35:12.234Z", - "modified": "2020-11-16T16:11:45.427Z", + "modified": "2021-10-22T15:25:58.218Z", "is_removed": false, "division": 1, "user": null, - "first_name": "\u5e95\u6ce2\u62c9", - "last_name": "Deborah", + "first_name": "Deborah", + "last_name": "", "first_name2": "\u5e95\u6ce2\u62c9", - "last_name2": null, + "last_name2": "", "gender": "FEMALE", "actual_birthday": null, "estimated_birthday": null, @@ -9533,14 +9680,12 @@ "fixed": {}, "names": { "nick": "\u6230\u795e\u4e48\u4e48", - "original": "\u5e95\u6ce2\u62c9 Deborah \u5e95\u6ce2\u62c9" + "original": "Deborah \u5e95\u6ce2\u62c9", + "simplified": "Deborah \u5e95\u6ce2\u62c9", + "traditional": "Deborah \u5e95\u6ce2\u62c9", + "romanization": "Deborah Di Bo La " }, - "contacts": { - "email1": null, - "email2": null, - "phone1": null, - "phone2": null - } + "contacts": {} } } }, @@ -9549,14 +9694,14 @@ "pk": "55ee6cde-b6ce-4551-b531-89507cee6ae1", "fields": { "created": "1999-11-16T04:02:53.691Z", - "modified": "2020-11-16T16:04:50.316Z", + "modified": "2021-10-22T15:26:24.178Z", "is_removed": false, "division": 3, "user": null, - "first_name": "\u4ee5\u6383", - "last_name": "Esau", - "first_name2": null, - "last_name2": null, + "first_name": "Esau", + "last_name": "", + "first_name2": "\u4ee5\u6383", + "last_name2": "", "gender": "MALE", "actual_birthday": null, "estimated_birthday": null, @@ -9566,14 +9711,12 @@ "infos": { "fixed": {}, "names": { - "original": "\u4ee5\u6383 Esau" + "original": "Esau \u4ee5\u6383", + "simplified": "Esau \u4ee5\u626b", + "traditional": "Esau \u4ee5\u6383", + "romanization": "Esau Yi Sao " }, - "contacts": { - "email1": null, - "email2": null, - "phone1": null, - "phone2": null - } + "contacts": {} } } }, @@ -9582,14 +9725,14 @@ "pk": "786a9d44-379f-4671-9004-5b3d0bd0227b", "fields": { "created": "1999-05-10T01:02:35.482Z", - "modified": "2020-11-16T16:12:51.991Z", + "modified": "2021-10-22T15:26:42.273Z", "is_removed": false, "division": 1, "user": null, "first_name": "Chileab", - "last_name": "\u57fa\u5229\u62bc", - "first_name2": null, - "last_name2": null, + "last_name": "", + "first_name2": "\u57fa\u5229\u62bc", + "last_name2": "", "gender": "MALE", "actual_birthday": null, "estimated_birthday": null, @@ -9599,14 +9742,12 @@ "infos": { "fixed": {}, "names": { - "original": "Chileab \u57fa\u5229\u62bc" + "original": "Chileab \u57fa\u5229\u62bc", + "simplified": "Chileab \u57fa\u5229\u62bc", + "traditional": "Chileab \u57fa\u5229\u62bc", + "romanization": "Chileab Ji Li Ya " }, - "contacts": { - "email1": null, - "email2": null, - "phone1": null, - "phone2": null - } + "contacts": {} } } }, @@ -9615,14 +9756,14 @@ "pk": "80f2e82f-0a72-49ea-b3e2-3fc892c49599", "fields": { "created": "1999-04-11T19:33:26.486Z", - "modified": "2020-11-16T16:01:35.300Z", + "modified": "2021-10-22T15:27:02.762Z", "is_removed": false, "division": 3, "user": null, - "first_name": "\u4ee5\u5be6\u746a\u5229", - "last_name": "Ishmael", - "first_name2": null, - "last_name2": null, + "first_name": "Ishmael", + "last_name": "", + "first_name2": "\u4ee5\u5be6\u746a\u5229", + "last_name2": "", "gender": "MALE", "actual_birthday": null, "estimated_birthday": "2010-02-04", @@ -9637,14 +9778,12 @@ }, "names": { "nick": "\u5927\u5bf6", - "original": "\u4ee5\u5be6\u746a\u5229 Ishmael" + "original": "Ishmael \u4ee5\u5be6\u746a\u5229", + "simplified": "Ishmael \u4ee5\u5b9e\u739b\u5229", + "traditional": "Ishmael \u4ee5\u5be6\u746a\u5229", + "romanization": "Ishmael Yi Shi Ma Li " }, - "contacts": { - "email1": null, - "email2": null, - "phone1": null, - "phone2": null - } + "contacts": {} } } }, @@ -9653,14 +9792,14 @@ "pk": "8b3b2130-9ec4-469b-9b64-efef141fceee", "fields": { "created": "1999-11-16T02:06:06.493Z", - "modified": "2020-11-16T16:05:13.871Z", + "modified": "2021-10-22T15:27:23.507Z", "is_removed": false, "division": 1, "user": null, "first_name": "Rebecca", - "last_name": "\u5229\u767e\u52a0", - "first_name2": null, - "last_name2": null, + "last_name": "", + "first_name2": "\u5229\u767e\u52a0", + "last_name2": "", "gender": "FEMALE", "actual_birthday": null, "estimated_birthday": null, @@ -9670,14 +9809,12 @@ "infos": { "fixed": {}, "names": { - "original": "Rebecca \u5229\u767e\u52a0" + "original": "Rebecca \u5229\u767e\u52a0", + "simplified": "Rebecca \u5229\u767e\u52a0", + "traditional": "Rebecca \u5229\u767e\u52a0", + "romanization": "Rebecca Li Bai Jia " }, - "contacts": { - "email1": null, - "email2": null, - "phone1": null, - "phone2": null - } + "contacts": {} } } }, @@ -9686,14 +9823,14 @@ "pk": "97922785-9025-40ad-9a5f-a9545f884644", "fields": { "created": "1999-05-10T01:00:09.119Z", - "modified": "2020-11-16T16:06:50.067Z", + "modified": "2021-10-22T15:27:39.124Z", "is_removed": false, "division": 6, "user": null, "first_name": "Nabal", - "last_name": "\u62ff\u516b", - "first_name2": null, - "last_name2": null, + "last_name": "", + "first_name2": "\u62ff\u516b", + "last_name2": "", "gender": "MALE", "actual_birthday": null, "estimated_birthday": null, @@ -9703,14 +9840,12 @@ "infos": { "fixed": {}, "names": { - "original": "Nabal \u62ff\u516b" + "original": "Nabal \u62ff\u516b", + "simplified": "Nabal \u62ff\u516b", + "traditional": "Nabal \u62ff\u516b", + "romanization": "Nabal Na Ba " }, - "contacts": { - "email1": null, - "email2": null, - "phone1": null, - "phone2": null - } + "contacts": {} } } }, @@ -9719,14 +9854,14 @@ "pk": "a48e4375-1a64-4c4f-beb3-8f088bf77340", "fields": { "created": "1999-04-11T19:31:50.659Z", - "modified": "2021-07-09T15:49:13.626Z", + "modified": "2021-10-22T15:28:28.385Z", "is_removed": false, "division": 1, "user": 1, - "first_name": "superuser1", - "last_name": "system admin1", - "first_name2": "superuser2", - "last_name2": "Lastnam2", + "first_name": "superuser", + "last_name": "system admin", + "first_name2": "\u7ba1\u7406\u54e1", + "last_name2": "\u8d85\u7d1a\u4f7f\u7528\u8005", "gender": "MALE", "actual_birthday": "1980-01-01", "estimated_birthday": null, @@ -9746,15 +9881,16 @@ }, "names": { "nick": "root", - "original": "superuser1 system admin1 Lastnam2superuser2", - "simplified": "Lastnam2superuser2", - "traditional": "Lastnam2superuser2" + "original": "superuser system admin \u8d85\u7d1a\u4f7f\u7528\u8005\u7ba1\u7406\u54e1", + "simplified": "superuser system admin \u8d85\u7ea7\u4f7f\u7528\u8005\u7ba1\u7406\u5458", + "traditional": "superuser system admin \u8d85\u7d1a\u4f7f\u7528\u8005\u7ba1\u7406\u54e1", + "romanization": "superuser system admin Chao Ji Shi Yong Zhe Guan Li Yuan " }, "contacts": { "email1": "5greata@email.com", "email2": "5greatb@email.com", - "phone1": "+15555555555", - "phone2": "+25555555555" + "phone1": "+1(555)555-5555", + "phone2": "+44(191)203-7010" } } } @@ -9764,13 +9900,13 @@ "pk": "aa24758f-cc70-4353-b1d7-9c616b3db425", "fields": { "created": "1999-04-11T19:38:16.323Z", - "modified": "2021-07-11T14:55:53.581Z", + "modified": "2021-10-22T15:28:56.077Z", "is_removed": false, "division": 1, "user": null, "first_name": "Abraham", - "last_name": "\u4e9e\u4f2f\u62c9\u7f55", - "first_name2": "", + "last_name": "", + "first_name2": "\u4e9e\u4f2f\u62c9\u7f55", "last_name2": "", "gender": "MALE", "actual_birthday": null, @@ -9785,14 +9921,15 @@ "names": { "nick": "", "original": "Abraham \u4e9e\u4f2f\u62c9\u7f55", - "simplified": "", - "traditional": "" + "simplified": "Abraham \u4e9a\u4f2f\u62c9\u7f55", + "traditional": "Abraham \u4e9e\u4f2f\u62c9\u7f55", + "romanization": "Abraham Ya Bo La Han " }, "contacts": { "email1": "6coola@email.com", "email2": "6coolb@email.com", - "phone1": "+16666666666", - "phone2": "+26666666666" + "phone1": "+1(666)666-6666", + "phone2": "+(86)6666-66666" } } } @@ -9802,14 +9939,14 @@ "pk": "be687517-d7bf-407a-8088-e3ed64ca62b8", "fields": { "created": "1999-11-16T02:06:58.862Z", - "modified": "2020-11-16T16:04:59.784Z", + "modified": "2021-10-22T15:29:18.343Z", "is_removed": false, "division": 3, "user": null, "first_name": "James", - "last_name": "\u96c5\u5404", - "first_name2": null, - "last_name2": null, + "last_name": "", + "first_name2": "\u96c5\u5404", + "last_name2": "", "gender": "MALE", "actual_birthday": null, "estimated_birthday": null, @@ -9819,14 +9956,12 @@ "infos": { "fixed": {}, "names": { - "original": "James \u96c5\u5404" + "original": "James \u96c5\u5404", + "simplified": "James \u96c5\u5404", + "traditional": "James \u96c5\u5404", + "romanization": "James Ya Ge " }, - "contacts": { - "email1": null, - "email2": null, - "phone1": null, - "phone2": null - } + "contacts": {} } } }, @@ -9835,14 +9970,14 @@ "pk": "d7e6ec14-302e-46a3-9f32-537d1fc0256b", "fields": { "created": "1999-04-11T19:37:45.378Z", - "modified": "2020-11-16T16:12:43.817Z", + "modified": "2021-10-22T15:29:43.443Z", "is_removed": false, "division": 3, "user": null, "first_name": "Lydia", - "last_name": "\u5442\u5e95\u4e9e", - "first_name2": null, - "last_name2": null, + "last_name": "", + "first_name2": "\u5442\u5e95\u4e9e", + "last_name2": "", "gender": "FEMALE", "actual_birthday": null, "estimated_birthday": null, @@ -9855,14 +9990,12 @@ "fixed": {}, "names": { "nick": "\u63a8\u96c5\u63a8\u5587\u7684\u5442\u5e95\u4e9e", - "original": "Lydia \u5442\u5e95\u4e9e" + "original": "Lydia \u5442\u5e95\u4e9e", + "simplified": "Lydia \u5415\u5e95\u4e9a", + "traditional": "Lydia \u5442\u5e95\u4e9e", + "romanization": "Lydia Lu Di Ya " }, - "contacts": { - "email1": null, - "email2": null, - "phone1": null, - "phone2": null - } + "contacts": {} } } }, @@ -9871,14 +10004,14 @@ "pk": "e8c025ec-9ea2-4d61-8df3-9daeecbfb849", "fields": { "created": "1999-04-11T19:34:03.252Z", - "modified": "2020-11-16T16:06:05.152Z", + "modified": "2021-10-22T15:30:15.077Z", "is_removed": false, "division": 3, "user": null, "first_name": "John", - "last_name": "\u7d04\u7ff0", - "first_name2": null, - "last_name2": null, + "last_name": "", + "first_name2": "\u7d04\u7ff0", + "last_name2": "", "gender": "MALE", "actual_birthday": null, "estimated_birthday": null, @@ -9889,14 +10022,12 @@ "fixed": {}, "names": { "nick": "\u4f7f\u5f92\u7d04\u7ff0", - "original": "John \u7d04\u7ff0" + "original": "John \u7d04\u7ff0", + "simplified": "John \u7ea6\u7ff0", + "traditional": "John \u7d04\u7ff0", + "romanization": "John Yue Han " }, - "contacts": { - "email1": null, - "email2": null, - "phone1": null, - "phone2": null - } + "contacts": {} } } }, @@ -9905,14 +10036,14 @@ "pk": "f3b780ad-c736-490e-8731-58b96dfd81f8", "fields": { "created": "1999-04-11T19:38:00.844Z", - "modified": "2020-11-16T16:04:32.705Z", + "modified": "2021-10-22T15:30:35.889Z", "is_removed": false, "division": 1, "user": null, "first_name": "Isaac", - "last_name": "\u4ee5\u6492", - "first_name2": null, - "last_name2": null, + "last_name": "", + "first_name2": "\u4ee5\u6492", + "last_name2": "", "gender": "MALE", "actual_birthday": null, "estimated_birthday": null, @@ -9922,13 +10053,16 @@ "infos": { "fixed": {}, "names": { - "original": "Isaac \u4ee5\u6492" + "original": "Isaac \u4ee5\u6492", + "simplified": "Isaac \u4ee5\u6492", + "traditional": "Isaac \u4ee5\u6492", + "romanization": "Isaac Yi Sa " }, "contacts": { "email1": "7coola@email.com", "email2": "7coolb@email.com", - "phone1": "+17777777777", - "phone2": "+27777777777" + "phone1": "+1(777)777-7777", + "phone2": "+27(77)777-7777" } } } @@ -9938,14 +10072,14 @@ "pk": "fa61941f-79a4-423e-872c-0bb9662e4351", "fields": { "created": "1999-04-11T19:36:42.735Z", - "modified": "2020-11-16T16:11:38.639Z", + "modified": "2021-10-22T15:30:57.300Z", "is_removed": false, "division": 2, "user": null, - "first_name": "\u4ee5\u65af\u5e16", - "last_name": "Esther", - "first_name2": null, - "last_name2": null, + "first_name": "Esther", + "last_name": "", + "first_name2": "\u4ee5\u65af\u5e16", + "last_name2": "", "gender": "FEMALE", "actual_birthday": null, "estimated_birthday": null, @@ -9956,14 +10090,12 @@ "fixed": {}, "names": { "nick": "\u7687\u540e", - "original": "\u4ee5\u65af\u5e16 Esther" + "original": "Esther \u4ee5\u65af\u5e16", + "simplified": "Esther \u4ee5\u65af\u5e16", + "traditional": "Esther \u4ee5\u65af\u5e16", + "romanization": "Esther Yi Si Tie " }, - "contacts": { - "email1": null, - "email2": null, - "phone1": null, - "phone2": null - } + "contacts": {} } } }, @@ -9972,14 +10104,14 @@ "pk": "ffd3881a-7a77-437a-ba11-c90610839523", "fields": { "created": "1999-04-11T19:38:35.150Z", - "modified": "2020-11-16T16:04:14.875Z", + "modified": "2021-10-22T15:31:20.648Z", "is_removed": false, "division": 1, "user": null, "first_name": "Sarah", - "last_name": "\u6492\u62c9", - "first_name2": null, - "last_name2": null, + "last_name": "", + "first_name2": "\u6492\u62c9", + "last_name2": "", "gender": "FEMALE", "actual_birthday": null, "estimated_birthday": null, @@ -9989,13 +10121,16 @@ "infos": { "fixed": {}, "names": { - "original": "Sarah \u6492\u62c9" + "original": "Sarah \u6492\u62c9", + "simplified": "Sarah \u6492\u62c9", + "traditional": "Sarah \u6492\u62c9", + "romanization": "Sarah Sa La " }, "contacts": { "email1": "8coola@email.com", "email2": "8coolb@email.com", - "phone1": "+18888888888", - "phone2": "+28888888888" + "phone1": "+1(888)888-8888", + "phone2": "+2(888)888-8888" } } }