Skip to content

Commit fbb1b88

Browse files
authored
Merge pull request #53 from toggle-corp/feature/daily-standup
Feature/daily standup
2 parents e062581 + b481329 commit fbb1b88

33 files changed

Lines changed: 1390 additions & 391 deletions

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/models.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class GoogleCalendarSyncStatus(models.IntegerChoices):
8282

8383
@typing.override
8484
def __str__(self):
85-
return self.name
85+
return f"{self.name} - {self.get_type_display()}"
8686

8787
@typing.override
8888
def save(self, *args, **kwargs):
@@ -181,6 +181,28 @@ def aget_last_working_date(
181181
) -> datetime.date: # type: ignore[reportReturnType]
182182
return cls.get_last_working_date(now_date, skip_dates=skip_dates, offset_count=offset_count)
183183

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+
184206
@classmethod
185207
def get_relative_non_working_events(cls) -> models.QuerySet["Event"]:
186208
"""

apps/project/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class DeadlineAdmin(VersionAdmin, UserResourceAdmin):
4343
AutocompleteFilterFactory("Project", "project"),
4444
AutocompleteFilterFactory("Contract", "contract"),
4545
)
46+
autocomplete_fields = ("contract", "project")
4647
actions = [sync_with_google_calendar]
4748

4849
@typing.override

apps/standup/admin.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,21 @@
22

33
from apps.common.admin import UserResourceAdmin, VersionAdmin
44

5-
from .models import Quote
5+
from .models import DailyUserStandup, Quote
66

77

88
@admin.register(Quote)
99
class QuoteAdmin(VersionAdmin, UserResourceAdmin):
10-
search_fields = ("author",)
10+
search_fields = ("author", "text")
1111
list_display = ("author", "last_viewed")
12+
13+
14+
@admin.register(DailyUserStandup)
15+
class DailyUserStandupAdmin(VersionAdmin):
16+
list_filter = ("date",)
17+
list_display = ("date", "conductor", "fallback_conductor")
18+
autocomplete_fields = (
19+
"quote",
20+
"conductor",
21+
"fallback_conductor",
22+
)

apps/standup/factories.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# pyright: reportPrivateImportUsage=false
2+
# pyright: reportIncompatibleVariableOverride=false
3+
import factory
4+
from factory.django import DjangoModelFactory
5+
6+
from .models import DailyUserStandup, Quote
7+
8+
9+
class DailyUserStandupFactory(DjangoModelFactory):
10+
class Meta:
11+
model = DailyUserStandup
12+
13+
14+
class QuoteFactory(DjangoModelFactory):
15+
author = factory.Faker("name")
16+
text = factory.Faker("sentence")
17+
18+
class Meta:
19+
model = Quote

apps/standup/management/__init__.py

Whitespace-only changes.

apps/standup/management/commands/__init__.py

Whitespace-only changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import logging
2+
import typing
3+
4+
from django.core.management.base import BaseCommand
5+
6+
from apps.standup.tasks import (
7+
before_standup_reminder,
8+
morning_reminder,
9+
read_doc_reminder,
10+
setup_next_standup,
11+
)
12+
13+
CommandActionType = typing.Literal[
14+
"morning-reminder",
15+
"before-standup-reminder",
16+
"setup-next-standup",
17+
"read-doc-reminder",
18+
]
19+
20+
logger = logging.getLogger(__name__)
21+
22+
23+
class Command(BaseCommand):
24+
help = "Daily Standup cli"
25+
26+
@typing.override
27+
def add_arguments(self, parser):
28+
subparsers = parser.add_subparsers(dest="action", help="Actions", required=True)
29+
30+
subparsers.add_parser("morning-reminder", help="Send morning reminder for conductors")
31+
subparsers.add_parser("before-standup-reminder", help="Send before standup reminder for the team")
32+
subparsers.add_parser("setup-next-standup", help="Setup next standup and assign conductors")
33+
subparsers.add_parser("read-doc-reminder", help="Send read doc reminder for conductors")
34+
35+
@typing.override
36+
def handle(self, action: CommandActionType, **_):
37+
match action:
38+
case "morning-reminder":
39+
return morning_reminder()
40+
case "before-standup-reminder":
41+
return before_standup_reminder()
42+
case "setup-next-standup":
43+
return setup_next_standup()
44+
case "read-doc-reminder":
45+
return read_doc_reminder()
46+
case _:
47+
typing.assert_never(action)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by Django 4.2.20 on 2025-04-29 13:50
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
('standup', '0002_quote_last_viewed'),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='DailyUserStandup',
18+
fields=[
19+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20+
('date', models.DateField(unique=True)),
21+
('slack_thread_ts', models.CharField(blank=True, max_length=20, null=True)),
22+
('conductor', models.ForeignKey(blank=True, help_text='User responsible for conducting the stand-up', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
23+
('fallback_conductor', models.ForeignKey(blank=True, help_text='Fallback user when conductor is not available', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
24+
('quote', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='standup.quote')),
25+
],
26+
),
27+
]

apps/standup/models.py

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,64 @@
1+
import textwrap
2+
import typing
3+
14
from django.db import models
5+
from django.db.models.functions import Now
6+
from django.utils.translation import gettext_lazy as _
27

38
from apps.common.models import UserResource
4-
5-
# from apps.user.models import User
6-
7-
8-
# class DailyUserStandup(models.Model):
9-
# user = models.ForeignKey(User, on_delete=models.CASCADE)
10-
# date = models.DateField()
11-
12-
# slack_thread_id = models.CharField(max_length=200) # TODO: Check length
13-
# text = models.TextField() # TODO: Do we need this?
9+
from apps.user.models import User
1410

1511

1612
class Quote(UserResource):
1713
text = models.TextField()
1814
author = models.CharField(max_length=225)
1915
last_viewed = models.DateTimeField(null=True, blank=True)
16+
17+
@classmethod
18+
def get_random_quote(cls, track_last_viewed=False) -> typing.Self | None:
19+
quote = cls.objects.order_by(
20+
models.F("last_viewed").asc(nulls_first=True),
21+
).first()
22+
if quote and track_last_viewed:
23+
cls.objects.filter(pk=quote.pk).update(last_viewed=Now())
24+
return quote
25+
26+
@typing.override
27+
def __str__(self):
28+
_text = textwrap.shorten(self.text, width=20, placeholder="...")
29+
return f"Quote: {self.author} - {_text}"
30+
31+
32+
# TODO: Add created_at, created_by, modified_by, modified_at
33+
class DailyUserStandup(models.Model):
34+
date = models.DateField(unique=True)
35+
36+
quote = models.ForeignKey(Quote, on_delete=models.SET_NULL, null=True, blank=True)
37+
38+
conductor = models.ForeignKey(
39+
User,
40+
help_text=_("User responsible for conducting the stand-up"),
41+
related_name="+",
42+
on_delete=models.SET_NULL,
43+
null=True,
44+
blank=True,
45+
)
46+
fallback_conductor = models.ForeignKey(
47+
User,
48+
help_text=_("Fallback user when conductor is not available"),
49+
related_name="+",
50+
on_delete=models.SET_NULL,
51+
null=True,
52+
blank=True,
53+
)
54+
55+
slack_thread_ts = models.CharField(max_length=20, null=True, blank=True)
56+
57+
# typing hints
58+
quote_id: int | None
59+
conductor_id: int | None
60+
fallback_conductor_id: int | None
61+
62+
@typing.override
63+
def __str__(self):
64+
return str(self.date)

0 commit comments

Comments
 (0)