Skip to content

Commit b16989e

Browse files
authored
Merge pull request #44 from toggle-corp/feature/settings-typed
### 🚀 Features - *(lint)* Enable reportImplicitOverride - *(standup)* Add DailyUserStandup model and Slack-related user fields - *(slack)* Add Slack bot configuration and utility class - *(standup)* Automate daily standup coordination and Slack messaging - *(cli)* Add management commands and cron jobs for standup and Slack sync ### 🐛 Bug Fixes - *(dev)* Exempt GraphiQL view from CSRF in debug mode ### 🚜 Refactor - Centralize settings access via typed config module - *(sso)* Update ui, css theming, and login session expiry indicator ### 🧪 Testing - *(standup,user)* Add tests for standup tasks, CLI commands, and Slack sync ### ⚙️ Miscellaneous Tasks - *(helm)* Update django-app chart dependency to v0.1.1
2 parents 01598bb + 08519ea commit b16989e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1868
-536
lines changed

apps/common/admin.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def has_delete_permission(self, request, obj=None):
2222

2323

2424
class UserResourceAdmin(admin.ModelAdmin):
25+
@typing.override
2526
def get_list_display(self, request):
2627
list_display = super().get_list_display(request)
2728
for field in ["created_by", "modified_by"]:
@@ -32,6 +33,7 @@ def get_list_display(self, request):
3233
]
3334
return list_display
3435

36+
@typing.override
3537
def get_readonly_fields(self, *args, **kwargs):
3638
readonly_fields = super().get_readonly_fields(*args, **kwargs) # type: ignore[reportAttributeAccessIssue]
3739
return [
@@ -47,12 +49,14 @@ def get_readonly_fields(self, *args, **kwargs):
4749
),
4850
]
4951

52+
@typing.override
5053
def save_model(self, request, obj, form, change):
5154
if not change:
5255
obj.created_by = request.user
5356
obj.modified_by = request.user
5457
super().save_model(request, obj, form, change) # type: ignore[reportAttributeAccessIssue]
5558

59+
@typing.override
5660
def save_formset(self, request, form, formset, change) -> None:
5761
if not issubclass(formset.model, UserResource):
5862
return super().save_formset(request, form, formset, change)
@@ -68,11 +72,13 @@ def save_formset(self, request, form, formset, change) -> None:
6872
instance.save()
6973
return None
7074

75+
@typing.override
7176
def get_queryset(self, request: HttpRequest) -> models.QuerySet[DjangoModel]:
7277
return super().get_queryset(request).select_related("created_by", "modified_by")
7378

7479

7580
class UserResourceTabularInline(admin.TabularInline):
81+
@typing.override
7682
def get_readonly_fields(self, *args, **kwargs):
7783
readonly_fields = super().get_readonly_fields(*args, **kwargs) # type: ignore[reportAttributeAccessIssue]
7884
return [
@@ -107,6 +113,7 @@ class EventAdmin(VersionAdmin, UserResourceAdmin):
107113
ordering = ("start_date",)
108114
actions = [sync_with_google_calendar]
109115

116+
@typing.override
110117
def get_readonly_fields(self, *args, **kwargs):
111118
readonly_fields = super().get_readonly_fields(*args, **kwargs) # type: ignore[reportAttributeAccessIssue]
112119
return [
@@ -121,6 +128,7 @@ def get_readonly_fields(self, *args, **kwargs):
121128
),
122129
]
123130

131+
@typing.override
124132
def save_model(self, request, obj, form, change):
125133
obj.google_calendar_sync_status = Event.GoogleCalendarSyncStatus.PENDING
126134
super().save_model(request, obj, form, change)

apps/common/factories.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# pyright: reportPrivateImportUsage=false
2+
# pyright: reportIncompatibleVariableOverride=false
3+
import factory
4+
from factory.django import DjangoModelFactory
5+
6+
from .models import Event
7+
8+
9+
class EventFactory(DjangoModelFactory):
10+
name = factory.Sequence(lambda n: f"Event-{n}")
11+
12+
class Meta:
13+
model = Event

apps/common/management/commands/google_calendar.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22
import logging
33
import typing
44

5-
from django.conf import settings
65
from django.core.management.base import BaseCommand
76

87
from apps.common.models import Event
98
from apps.common.tasks import sync_event_with_google_calendar
10-
from apps.common.utils.google_calendar import GoogleCalendarShareRoleType, GoogleServiceAccount
9+
from apps.common.utils.google_calendar import (
10+
GoogleCalendarInitialisationError,
11+
GoogleCalendarShareRoleType,
12+
GoogleServiceAccount,
13+
)
1114
from apps.project.models import Deadline
1215
from apps.project.tasks import sync_deadline_with_google_calendar
16+
from main import config
1317

1418
CommandActionType = typing.Literal[
1519
"list-calendars",
@@ -51,10 +55,18 @@ def list_all_events(service: GoogleServiceAccount, calendar_id, time_min: str |
5155
class Command(BaseCommand):
5256
help = "Initialize google calendar"
5357

58+
def __init__(self, *args, **kwargs):
59+
if config.GOOGLE_CALENDAR_ID is None:
60+
raise GoogleCalendarInitialisationError("GOOGLE_CALENDAR_ID is not defined")
61+
62+
self.calendar_id = config.GOOGLE_CALENDAR_ID
63+
return super().__init__(*args, **kwargs)
64+
5465
def confirm(self, message: str) -> bool:
5566
prompt = self.style.NOTICE(f"{message} (y/n): ")
5667
return input(prompt).strip().lower() == "y"
5768

69+
@typing.override
5870
def add_arguments(self, parser):
5971
subparsers = parser.add_subparsers(dest="action", help="Actions", required=True)
6072

@@ -101,7 +113,7 @@ def add_arguments(self, parser):
101113
subparsers.add_parser("reset-timur-data", help="List calendars")
102114

103115
def list_calendars(self, service: GoogleServiceAccount):
104-
logger.info("Action calendar ID: %s", settings.GOOGLE_CALENDAR_ID)
116+
logger.info("Action calendar ID: %s", self.calendar_id)
105117
for cal in service.list_calendar():
106118
self.stdout.write(json.dumps(cal, indent=2))
107119

@@ -118,7 +130,7 @@ def share_calendar(
118130
role = typing.cast("GoogleCalendarShareRoleType", options["role"])
119131
calendar_id = typing.cast(
120132
"str",
121-
options.get("calendar_id") or settings.GOOGLE_CALENDAR_ID,
133+
options.get("calendar_id") or self.calendar_id,
122134
)
123135

124136
if role == "owner":
@@ -150,7 +162,7 @@ def list_events(self, service: GoogleServiceAccount, **options):
150162
time_min = options["time_min"]
151163
google_calendar_events = list_all_events(
152164
service,
153-
calendar_id=settings.GOOGLE_CALENDAR_ID,
165+
calendar_id=self.calendar_id,
154166
time_min=time_min,
155167
)
156168
for result in google_calendar_events:
@@ -161,7 +173,7 @@ def list_colors(self, service: GoogleServiceAccount):
161173
self.stdout.write(json.dumps(list(results.items()), indent=2))
162174

163175
def list_calendar_access(self, service: GoogleServiceAccount, **options):
164-
calendar_id = options.get("calendar_id") or settings.GOOGLE_CALENDAR_ID
176+
calendar_id = options.get("calendar_id") or self.calendar_id
165177
compact = options.get("compact", False)
166178

167179
results = service.service_account.acl().list(calendarId=calendar_id).execute()
@@ -183,7 +195,7 @@ def delete_event(self, service: GoogleServiceAccount, **options):
183195

184196
if self.confirm("Are you sure?"):
185197
service.service_account.events().delete(
186-
calendarId=settings.GOOGLE_CALENDAR_ID,
198+
calendarId=self.calendar_id,
187199
eventId=event_id,
188200
).execute()
189201

@@ -252,7 +264,7 @@ def reset_timur_data(self, service: GoogleServiceAccount):
252264
"deadlines": to_process_deadline_qs.count(),
253265
}
254266

255-
calendar_id = settings.GOOGLE_CALENDAR_ID
267+
calendar_id = self.calendar_id
256268
if not self.confirm(f"Are you sure? This will remove {summary}"):
257269
return
258270

@@ -288,6 +300,7 @@ def reset_timur_data(self, service: GoogleServiceAccount):
288300
self.stdout.write(f" - Success {events_resp}")
289301
self.stdout.write(f" - Success {deadline_resp}")
290302

303+
@typing.override
291304
def handle(self, action: CommandActionType, **options):
292305
gsc = GoogleServiceAccount()
293306

apps/common/management/commands/graphql_schema.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import argparse
2+
import typing
23

34
from django.core.management.base import BaseCommand
45
from strawberry.printer import print_schema
@@ -9,13 +10,15 @@
910
class Command(BaseCommand):
1011
help = "Create schema.graphql file"
1112

13+
@typing.override
1214
def add_arguments(self, parser):
1315
parser.add_argument(
1416
"--out",
1517
type=argparse.FileType("w"),
1618
default="schema.graphql",
1719
)
1820

21+
@typing.override
1922
def handle(self, *args, **options):
2023
file = options["out"]
2124
file.write(print_schema(schema))

apps/common/management/commands/init_development.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import dataclasses
22
import functools
33
import json
4+
import typing
45
from argparse import FileType
56
from sys import stdin
67

@@ -64,11 +65,13 @@ def wrapper(*args, **kwargs):
6465
class Command(BaseCommand):
6566
help = "Generate data for development"
6667

68+
@typing.override
6769
def add_arguments(self, parser):
6870
parser.add_argument("input_file", nargs="?", type=FileType("r"), default=stdin)
6971
# TODO: Generate time track data
7072
# TODO: Generate journal data
7173

74+
@typing.override
7275
def handle(self, **options):
7376
if not (settings.DEBUG and settings.ALLOW_DUMMY_DATA_SCRIPT):
7477
self.stdout.write(

apps/common/management/commands/wait_for_resources.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import signal
22
import time
3+
import typing
34
from urllib.parse import urljoin
45

56
import requests
@@ -104,6 +105,7 @@ def wait_for_minio(self):
104105

105106
self.stdout.write(self.style.SUCCESS(f"Minio is available after {retry_helper.total_time()} seconds"))
106107

108+
@typing.override
107109
def add_arguments(self, parser):
108110
parser.add_argument(
109111
"--timeout",
@@ -116,6 +118,7 @@ def add_arguments(self, parser):
116118
parser.add_argument("--minio", action="store_true", help="Wait for MinIO (S3) storage to be available")
117119
parser.add_argument("--all", action="store_true", help="Wait for all to be available")
118120

121+
@typing.override
119122
def handle(self, **kwargs):
120123
timeout = kwargs["timeout"]
121124
_all = kwargs["all"]

apps/common/models.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,16 @@ class GoogleCalendarSyncStatus(models.IntegerChoices):
8080
get_type_display: typing.Callable[..., str]
8181
get_google_calendar_sync_status_display: typing.Callable[..., str]
8282

83+
@typing.override
8384
def __str__(self):
84-
return self.name
85+
return f"{self.name} - {self.get_type_display()}"
8586

87+
@typing.override
8688
def save(self, *args, **kwargs):
8789
self.reload_cache()
8890
return super().save(*args, **kwargs)
8991

92+
@typing.override
9093
def delete(self, *args, **kwargs):
9194
from apps.common.tasks import delete_event_from_google_calendar
9295

@@ -106,6 +109,7 @@ def dates_check(self):
106109
if self.start_date > self.end_date:
107110
raise ValidationError(_("Start date can't be greater then End date"))
108111

112+
@typing.override
109113
def clean(self):
110114
super().clean()
111115
self.dates_check()
@@ -177,6 +181,28 @@ def aget_last_working_date(
177181
) -> datetime.date: # type: ignore[reportReturnType]
178182
return cls.get_last_working_date(now_date, skip_dates=skip_dates, offset_count=offset_count)
179183

184+
@classmethod
185+
def get_next_working_date(
186+
cls,
187+
now_date: datetime.date,
188+
skip_dates: list[datetime.date] | None = None,
189+
offset_count: int | None = None,
190+
) -> datetime.date: # type: ignore[reportReturnType]
191+
# TODO: Add test
192+
dates_to_skip = set(cls.get_relative_event_dates())
193+
if skip_dates:
194+
dates_to_skip.update(skip_dates)
195+
found_count = 0
196+
for x in range(30): # Create a 1 month window, Should be enough
197+
date = now_date + datetime.timedelta(days=x)
198+
if cls.is_weekend(date) or date in dates_to_skip:
199+
continue
200+
if offset_count is not None and found_count < offset_count:
201+
found_count += 1
202+
continue
203+
return date
204+
return timezone.now() # XXX: Fallback to now
205+
180206
@classmethod
181207
def get_relative_non_working_events(cls) -> models.QuerySet["Event"]:
182208
"""

apps/common/serializers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import typing
2+
13
from django.core.exceptions import FieldDoesNotExist
24
from rest_framework import serializers
35

@@ -12,13 +14,15 @@ class UserResourceSerializer(serializers.ModelSerializer):
1214
client_id = StringIDField(required=False)
1315
version_id = serializers.SerializerMethodField()
1416

17+
@typing.override
1518
def create(self, validated_data):
1619
if "created_by" in self.Meta.model._meta._forward_fields_map: # type: ignore[reportAttributeAccessIssue]
1720
validated_data["created_by"] = self.context["request"].user
1821
if "modified_by" in self.Meta.model._meta._forward_fields_map: # type: ignore[reportAttributeAccessIssue]
1922
validated_data["modified_by"] = self.context["request"].user
2023
return super().create(validated_data)
2124

25+
@typing.override
2226
def update(self, instance, validated_data):
2327
if "modified_by" in self.Meta.model._meta._forward_fields_map: # type: ignore[reportAttributeAccessIssue]
2428
validated_data["modified_by"] = self.context["request"].user
@@ -54,6 +58,7 @@ def _get_temp_client_id(self, validated_data):
5458
# If we don't remove `client_id` from validated_data, then serializer will throw error on update/create
5559
return validated_data.pop("client_id", None)
5660

61+
@typing.override
5762
def create(self, validated_data):
5863
temp_client_id = self._get_temp_client_id(validated_data)
5964
instance = super().create(validated_data)
@@ -62,6 +67,7 @@ def create(self, validated_data):
6267
local_cache.set(self.get_cache_key(instance, self.context["request"]), temp_client_id, 60)
6368
return instance
6469

70+
@typing.override
6571
def update(self, instance, validated_data):
6672
temp_client_id = self._get_temp_client_id(validated_data)
6773
instance = super().update(instance, validated_data)

0 commit comments

Comments
 (0)