From 6455c8e9b97447322844ba2198aecc2c4d2050d0 Mon Sep 17 00:00:00 2001 From: Michael Romashov Date: Fri, 3 Nov 2023 00:24:45 -0400 Subject: [PATCH 1/5] feat: quarterly controlling hours --- .../commands/notify_inactive_controllers.py | 8 ++-- apps/connections/serializers.py | 11 ++--- apps/connections/statistics.py | 42 +++++++++---------- apps/users/models.py | 6 +-- emails/activity_reminder.html | 4 +- emails/activity_reminder.txt | 4 +- 6 files changed, 39 insertions(+), 36 deletions(-) diff --git a/apps/connections/management/commands/notify_inactive_controllers.py b/apps/connections/management/commands/notify_inactive_controllers.py index e355234..2234139 100644 --- a/apps/connections/management/commands/notify_inactive_controllers.py +++ b/apps/connections/management/commands/notify_inactive_controllers.py @@ -16,13 +16,15 @@ def handle(self, *args, **options): return hours = get_user_hours() - month_name = datetime.now().strftime('%B') + curr_date = datetime.today() + current_quarter = (curr_date.month - 1) // 3 + 1 for user in hours.filter(status=Status.ACTIVE, roles__short__in=['HC', 'VC']): - if user.curr_hours < user.activity_requirement: + if getattr(user, f'q{current_quarter}') < user.activity_requirement: context = { 'user': user, - 'month': month_name, + 'quarter': current_quarter, + 'year': curr_date.year, } Email( subject='Controller Activity Reminder', diff --git a/apps/connections/serializers.py b/apps/connections/serializers.py index 5b109af..96dab5f 100644 --- a/apps/connections/serializers.py +++ b/apps/connections/serializers.py @@ -21,15 +21,16 @@ class Meta: class StatisticsSerializer(serializers.ModelSerializer): - curr_hours = CustomDurationField() - prev_hours = CustomDurationField() - prev_prev_hours = CustomDurationField() + q1 = CustomDurationField() + q2 = CustomDurationField() + q3 = CustomDurationField() + q4 = CustomDurationField() activity_requirement = CustomDurationField() class Meta: model = User - fields = ['cid', 'first_name', 'last_name', 'rating', 'curr_hours', 'initials', - 'prev_hours', 'prev_prev_hours', 'activity_requirement'] + fields = ['cid', 'first_name', 'last_name', 'rating', 'initials', + 'q1', 'q2', 'q3', 'q4', 'activity_requirement'] class TopControllersSerializer(serializers.ModelSerializer): diff --git a/apps/connections/statistics.py b/apps/connections/statistics.py index f56291a..bddd079 100644 --- a/apps/connections/statistics.py +++ b/apps/connections/statistics.py @@ -13,19 +13,13 @@ def annotate_hours(query): current (curr_hours), previous (prev_hours), and penultimate (prev_prev_hours) months. """ - MONTH_NOW = timezone.now().month - YEAR_NOW = timezone.now().year - CURR_MONTH = (Q(sessions__start__month=MONTH_NOW) - & Q(sessions__start__year=YEAR_NOW)) - PREV_MONTH = (Q(sessions__start__month=MONTH_NOW - 1 if MONTH_NOW > 1 else 12) - & Q(sessions__start__year=YEAR_NOW if MONTH_NOW > 1 else YEAR_NOW - 1)) - PREV_PREV_MONTH = (Q(sessions__start__month=MONTH_NOW - 2 if MONTH_NOW > 2 else 12 if MONTH_NOW > 1 else 11) - & Q(sessions__start__year=YEAR_NOW if MONTH_NOW > 2 else YEAR_NOW - 1)) + is_curr_year = Q(sessions__start__year=timezone.now().year) return query.annotate( - curr_hours=Coalesce(Sum('sessions__duration', filter=CURR_MONTH), Cast(timedelta(), DurationField())), - prev_hours=Coalesce(Sum('sessions__duration', filter=PREV_MONTH), Cast(timedelta(), DurationField())), - prev_prev_hours=Coalesce(Sum('sessions__duration', filter=PREV_PREV_MONTH), Cast(timedelta(), DurationField())), + q1=Coalesce(Sum('sessions__duration', filter=Q(sessions__start__quarter=1) & is_curr_year), Cast(timedelta(), DurationField())), + q2=Coalesce(Sum('sessions__duration', filter=Q(sessions__start__quarter=2) & is_curr_year), Cast(timedelta(), DurationField())), + q3=Coalesce(Sum('sessions__duration', filter=Q(sessions__start__quarter=3) & is_curr_year), Cast(timedelta(), DurationField())), + q4=Coalesce(Sum('sessions__duration', filter=Q(sessions__start__quarter=4) & is_curr_year), Cast(timedelta(), DurationField())), ) @@ -44,20 +38,26 @@ def get_top_controllers(): hour sums for the current month (hours) sorted by most controlling hours (controllers with no hours are not included). """ - SAME_MONTH = Q(sessions__start__month=timezone.now().month) - SAME_YEAR = Q(sessions__start__year=timezone.now().year) - - users = User.objects.exclude(status=Status.NON_MEMBER) - users = users.annotate(hours=Sum('sessions__duration', filter=SAME_MONTH & SAME_YEAR)) - - return users.exclude(hours__isnull=True).order_by('-hours') + curr_time = timezone.now() + + return ( + User.objects + .exclude(status=Status.NON_MEMBER) + .annotate( + hours=Sum( + 'sessions__duration', + filter=Q(sessions__start__month=curr_time.month) & Q(sessions__start__year=curr_time.year) + ) + ) + .exclude(hours__isnull=True) + .order_by('-hours') + ) def get_top_positions(): - SAME_MONTH = Q(start__month=timezone.now().month) - SAME_YEAR = Q(start__year=timezone.now().year) + curr_time = timezone.now() - sessions = ControllerSession.objects.filter(SAME_MONTH & SAME_YEAR) + sessions = ControllerSession.objects.filter(start__month=curr_time.month, start__year=curr_time.year) position_durations = {} for session in sessions: diff --git a/apps/users/models.py b/apps/users/models.py index a0f10d9..6d7a910 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -154,9 +154,9 @@ def activity_requirement(self): if self.del_cert == Certification.NONE: return timedelta(hours=0) elif self.is_staff: - return timedelta(hours=5) + return timedelta(hours=6) else: - return timedelta(hours=2) + return timedelta(hours=3) @property def visiting_eligibility(self): @@ -310,7 +310,7 @@ def set_membership(self, short, override=True): if override: self.initials = self.get_initials() self.joined = timezone.now() - + self.profile = self.generate_profile() if os.getenv('DEV_ENV') == 'False': diff --git a/emails/activity_reminder.html b/emails/activity_reminder.html index 99a2b0f..56dceb1 100644 --- a/emails/activity_reminder.html +++ b/emails/activity_reminder.html @@ -8,7 +8,7 @@

Hello {{ user.first_name }},

-

You are receiving this email to inform you that you have not fulfilled your monthly activity requirements. In the month of {{ month }}, you have controlled for {{ user.curr_hours|format_timedelta }}. To maintain roster currency you need to control for {{ user.activity_requirement|timedelta_hours }} hours.

+

You are receiving this email to inform you that you have not fulfilled your monthly activity requirements. During Q{{ quarter }} {{ year }}, you have controlled for {{ user.curr_hours|format_timedelta }}. To maintain roster currency you need to control for {{ user.activity_requirement|timedelta_hours }} hours.

@@ -21,4 +21,4 @@

Hello {{ user.first_name }},

If you are unable to fulfill your activity requirements or if you have any questions, don't hesitate to reach out to datm@zhuartcc.org.

-
\ No newline at end of file +
diff --git a/emails/activity_reminder.txt b/emails/activity_reminder.txt index f52b0ab..2245790 100644 --- a/emails/activity_reminder.txt +++ b/emails/activity_reminder.txt @@ -2,11 +2,11 @@ Hello {{ user.first_name }}, -You are receiving this email to inform you that you have not fulfilled your monthly activity requirements. In the month of {{ month }}, you have controlled for {{ user.curr_hours|format_timedelta }}. To maintain roster currency you need to control for {{ user.activity_requirement|timedelta_hours }} hours. +You are receiving this email to inform you that you have not fulfilled your monthly activity requirements. During Q{{ quarter }} {{ year }}, you have controlled for {{ user.curr_hours|format_timedelta }}. To maintain roster currency you need to control for {{ user.activity_requirement|timedelta_hours }} hours. Not fulfilling your hours by the end of the month puts you at risk for removal from the Houston ARTCC roster. If you are unable to fulfill your activity requirements or if you have any questions, don't hesitate to reach out to datm@zhuartcc.org. -If you would like to edit your email preferences, you can do so at https://zhuartcc.org/settings. \ No newline at end of file +If you would like to edit your email preferences, you can do so at https://zhuartcc.org/settings. From 841a82461fef1d9d5ae495da4fe0d2acda7d1798 Mon Sep 17 00:00:00 2001 From: Michael Romashov Date: Wed, 29 Nov 2023 12:42:33 -0500 Subject: [PATCH 2/5] feat: sync visiting roster --- .../management/commands/sync_vatusa_roster.py | 69 ++++++++++++------- apps/users/models.py | 6 ++ apps/visit/views.py | 4 -- zhu_core/utils.py | 4 +- 4 files changed, 53 insertions(+), 30 deletions(-) diff --git a/apps/users/management/commands/sync_vatusa_roster.py b/apps/users/management/commands/sync_vatusa_roster.py index 3d56814..5598369 100644 --- a/apps/users/management/commands/sync_vatusa_roster.py +++ b/apps/users/management/commands/sync_vatusa_roster.py @@ -1,37 +1,58 @@ +import os from datetime import datetime + +import requests from django.core.management.base import BaseCommand from zhu_core.utils import get_vatusa_roster from apps.users.models import User +def sync_home_roster(): + home_roster = get_vatusa_roster() + + # Checks for users that do not exist on local roster. + for user in home_roster: + query = User.objects.filter(cid=user.get('cid')) + if not query.exists(): + User.objects.create_user( + cid=user.get('cid'), + email=user.get('email'), + first_name=user.get('fname'), + last_name=user.get('lname'), + rating=user.get('rating_short'), + ).set_membership('HC') + else: + user_obj = query.first() + user_obj.rating = user.get('rating_short') + user_obj.save() + user_obj.set_membership('HC') + + # Checks for users that were removed from VATUSA roster. + cids = [user.get('cid') for user in home_roster] + for user in User.objects.filter(roles__short='HC'): + if user.cid not in cids: + user.set_membership(None) + + +def sync_visit_roster(): + visit_roster = get_vatusa_roster('visit') + remote_cids = {user['cid'] for user in visit_roster} + + # Users that have been added to the local roster but not the VATUSA roster. + for local_cid in User.objects.filter(roles__short='VC').values('cid', flat=True): + if local_cid not in remote_cids: + requests.post( + f'https://api.vatusa.net/v2/facility/{os.getenv("FACILITY_IATA")}/roster/manageVisitor/{local_cid}/', + params={'apikey': os.getenv('VATUSA_API_TOKEN')}, + ) + + class Command(BaseCommand): help = 'Pulls VATUSA roster for configured facility' def handle(self, *args, **options): - roster = get_vatusa_roster() - - # Checks for users that do not exist on local roster. - for user in roster: - query = User.objects.filter(cid=user.get('cid')) - if not query.exists(): - User.objects.create_user( - cid=user.get('cid'), - email=user.get('email'), - first_name=user.get('fname'), - last_name=user.get('lname'), - rating=user.get('rating_short'), - ).set_membership('HC') - else: - user_obj = query.first() - user_obj.rating = user.get('rating_short') - user_obj.save() - user_obj.set_membership('HC') - - # Checks for users that were removed from VATUSA roster. - cids = [user.get('cid') for user in roster] - for user in User.objects.filter(roles__short='HC'): - if user.cid not in cids: - user.set_membership(None) + sync_home_roster() + sync_visit_roster() print(f'{datetime.now()} :: sync_vatusa_roster :: SUCCESS') diff --git a/apps/users/models.py b/apps/users/models.py index 6d7a910..ef80bdd 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -319,6 +319,12 @@ def set_membership(self, short, override=True): if short == 'HC': self.home_facility = 'ZHU' + if short == 'VC': + requests.post( + f'https://api.vatusa.net/v2/facility/{os.getenv("FACILITY_IATA")}/roster/manageVisitor/{self.cid}/', + params={'apikey': os.getenv('VATUSA_API_TOKEN')}, + ) + self.status = Status.ACTIVE self.save() diff --git a/apps/visit/views.py b/apps/visit/views.py index b563081..becc6c7 100644 --- a/apps/visit/views.py +++ b/apps/visit/views.py @@ -52,10 +52,6 @@ def put(self, request, application_id): """ application = get_object_or_404(VisitingApplication, id=application_id) application.user.set_membership('VC') - requests.post( - f'https://api.vatusa.net/v2/facility/{os.getenv("FACILITY_IATA")}' - f'/roster/manageVisitor/{application.user.cid}/' - ) application.delete() return Response(status=status.HTTP_200_OK) diff --git a/zhu_core/utils.py b/zhu_core/utils.py index 480f611..b013ec8 100644 --- a/zhu_core/utils.py +++ b/zhu_core/utils.py @@ -34,9 +34,9 @@ def get_vatsim_data(): return resp.json() -def get_vatusa_roster(): +def get_vatusa_roster(membership='home'): resp = requests.get( - f'https://api.vatusa.net/v2/facility/{os.getenv("FACILITY_IATA")}/roster', + f'https://api.vatusa.net/v2/facility/{os.getenv("FACILITY_IATA")}/roster/{membership}', params={'apikey': os.getenv('VATUSA_API_TOKEN')}, ) assert resp.status_code == 200, 'Error pulling VATUSA roster.' From 6b4cc5ce0b1308ab553ed135c5508a325785c3d6 Mon Sep 17 00:00:00 2001 From: Michael Romashov Date: Wed, 29 Nov 2023 12:44:08 -0500 Subject: [PATCH 3/5] fix: wrong function call --- apps/users/management/commands/sync_vatusa_roster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/users/management/commands/sync_vatusa_roster.py b/apps/users/management/commands/sync_vatusa_roster.py index 5598369..f13da68 100644 --- a/apps/users/management/commands/sync_vatusa_roster.py +++ b/apps/users/management/commands/sync_vatusa_roster.py @@ -40,7 +40,7 @@ def sync_visit_roster(): remote_cids = {user['cid'] for user in visit_roster} # Users that have been added to the local roster but not the VATUSA roster. - for local_cid in User.objects.filter(roles__short='VC').values('cid', flat=True): + for local_cid in User.objects.filter(roles__short='VC').values_list('cid', flat=True): if local_cid not in remote_cids: requests.post( f'https://api.vatusa.net/v2/facility/{os.getenv("FACILITY_IATA")}/roster/manageVisitor/{local_cid}/', From 2a58fa9b442dbf0cac0fc27fd8cd0716bf4f554d Mon Sep 17 00:00:00 2001 From: Michael Romashov Date: Thu, 8 Feb 2024 16:33:18 -0500 Subject: [PATCH 4/5] feat: forward vatis requests to simtraffic --- apps/tmu/views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/tmu/views.py b/apps/tmu/views.py index 4161a63..08b4620 100644 --- a/apps/tmu/views.py +++ b/apps/tmu/views.py @@ -1,3 +1,5 @@ +import requests + from rest_framework import status from rest_framework.authentication import TokenAuthentication from rest_framework.generics import get_object_or_404 @@ -24,7 +26,10 @@ def post(self, request): """ Create new ATIS (from vATIS). """ - ATIS.objects.filter(facility=request.data.get('facility')).delete() + # Forward request to SimTraffic so the data is replicated there + requests.post("https://api.simtraffic.net/vatis", data=request.data) + + ATIS.objects.filter(facility=request.data.get("facility")).delete() serializer = ATISSerializer(data=request.data) if serializer.is_valid(): serializer.save() From 461e53bd2c149c9c844fdfb772f487bf8947ecaa Mon Sep 17 00:00:00 2001 From: Michael Romashov Date: Sun, 18 Feb 2024 00:29:54 -0500 Subject: [PATCH 5/5] fix: remove references to TS server --- emails/welcome_email.html | 15 --------------- emails/welcome_email.txt | 6 ------ 2 files changed, 21 deletions(-) diff --git a/emails/welcome_email.html b/emails/welcome_email.html index cb51d1f..2dea303 100644 --- a/emails/welcome_email.html +++ b/emails/welcome_email.html @@ -29,21 +29,6 @@

Discord

Please join our Discord server by clicking this link or using the invite code 93225ukwrW.

- - -

TeamSpeak

- - - - -

TeamSpeak is our primary voice communications platform. While on the network, all controllers must be in a TeamSpeak channel for coordination. TeamSpeak 3 can be downloaded at https://www.teamspeak.com/en.

- - - - -

You can connect to our server by using the IP ts.zhuartcc.org. Use your full name as your nickname when you connect. Please do not share the server IP with anybody outside of the Houston ARTCC.

- -

Website

diff --git a/emails/welcome_email.txt b/emails/welcome_email.txt index a5446a5..7611d6b 100644 --- a/emails/welcome_email.txt +++ b/emails/welcome_email.txt @@ -11,12 +11,6 @@ We use Discord as our primary text communications platform. All ARTCC announceme Please join our Discord server by visiting https://discord.gg/93225ukwrW or using the invite code 93225ukwrW. -TeamSpeak -TeamSpeak is our primary voice communications platform. While on the network, all controllers must be in a TeamSpeak channel for coordination. TeamSpeak 3 can be downloaded at https://www.teamspeak.com/en. - -You can connect to our server by using the IP ts.zhuartcc.org. Use your full name as your nickname when you connect. Please do not share the server IP with anybody outside of the Houston ARTCC. - - Website Upon receiving this email, you should be able to sign in at https://zhuartcc.org using VATSIM Connect. Here you can access controlling files and facility documents, the training center, event shift bookings, and see controlling statistics.