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

Past to meet #26

Merged
merged 12 commits into from
Oct 26, 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
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,13 @@ [email protected]
- [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
Expand All @@ -269,14 +271,14 @@ [email protected]
- [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
Expand All @@ -287,9 +289,10 @@ [email protected]
- [ ] 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
Expand Down
4 changes: 2 additions & 2 deletions attendees/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
4 changes: 2 additions & 2 deletions attendees/occasions/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
}),
)

Expand Down
2 changes: 1 addition & 1 deletion attendees/occasions/migrations/0002_assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
),
Expand Down
2 changes: 1 addition & 1 deletion attendees/occasions/migrations/0004_character.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
),
Expand Down
1 change: 1 addition & 0 deletions attendees/occasions/migrations/0005_meet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down
2 changes: 1 addition & 1 deletion attendees/occasions/models/assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -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', ),
]
Expand Down
2 changes: 1 addition & 1 deletion attendees/occasions/models/character.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 '')
1 change: 1 addition & 0 deletions attendees/occasions/models/meet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions attendees/persons/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.')
Expand Down Expand Up @@ -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.')
Expand Down
4 changes: 4 additions & 0 deletions attendees/persons/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions attendees/persons/models/attendee.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 27 additions & 2 deletions attendees/persons/models/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {},
Expand Down Expand Up @@ -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):
Expand Down
106 changes: 106 additions & 0 deletions attendees/persons/signals.py
Original file line number Diff line number Diff line change
@@ -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,
)
2 changes: 0 additions & 2 deletions attendees/persons/views/api/categorized_pasts.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading