Skip to content

Commit d1db322

Browse files
authored
Merge pull request #87 from toggle-corp/project/better-journaling
Project: Better journaling
2 parents 387ece3 + 5ba2356 commit d1db322

7 files changed

Lines changed: 149 additions & 8 deletions

File tree

apps/common/filters.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import strawberry
22
import strawberry_django
3+
from django.db import models
34

45
from .enums import EventTypeEnum
56
from .models import Event
@@ -10,4 +11,12 @@ class EventFilter:
1011
id: strawberry.auto
1112
start_date: strawberry.auto
1213
end_date: strawberry.auto
13-
types: list[EventTypeEnum] # type: ignore[reportInvalidTypeForm]
14+
15+
@strawberry_django.filter_field
16+
def types(
17+
self,
18+
queryset: models.QuerySet,
19+
value: list[EventTypeEnum], # type: ignore[reportInvalidTypeForm]
20+
prefix: str,
21+
) -> tuple[models.QuerySet, models.Q]:
22+
return queryset, models.Q(**{f"{prefix}type__in": value})

apps/project/filters.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import strawberry
22
import strawberry_django
33

4-
from .models import Client, Contractor, Project
4+
from .models import Client, Contractor, Deadline, Project
55

66

77
@strawberry_django.filters.filter(Client, lookups=True)
@@ -21,3 +21,10 @@ class ProjectFilter:
2121
id: strawberry.auto
2222
project_client: strawberry.auto
2323
contractor: strawberry.auto
24+
25+
26+
@strawberry_django.filters.filter(Deadline, lookups=True)
27+
class DeadlineFilter:
28+
id: strawberry.auto
29+
end_date: strawberry.auto
30+
is_archived: strawberry.auto

apps/project/queries.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import strawberry
22
import strawberry_django
3+
from strawberry_django.filters import apply as apply_filters
34

45
from main.graphql.context import Info
56
from utils.strawberry.paginations import CountList, pagination_field
67

7-
from .filters import ClientFilter, ContractorFilter, ProjectFilter
8+
from .filters import ClientFilter, ContractorFilter, DeadlineFilter, ProjectFilter
89
from .orders import ClientOrder, ContractorOrder, ProjectOrder
910
from .types import ClientType, ContractorType, DeadlineType, ProjectType
1011

@@ -37,8 +38,13 @@ async def all_projects(self, info: Info) -> list[ProjectType]:
3738
return [project async for project in qs]
3839

3940
@strawberry_django.field
40-
async def all_deadlines(self, info: Info) -> list[DeadlineType]:
41-
qs = DeadlineType.get_queryset(None, None, info).filter(is_archived=False).all()
41+
async def all_deadlines(self, info: Info, filters: DeadlineFilter | None = None) -> list[DeadlineType]:
42+
# NOTE: filters is temporarily optional
43+
qs = DeadlineType.get_queryset(None, None, info)
44+
if filters is None or filters.is_archived is strawberry.UNSET:
45+
qs = qs.filter(is_archived=False)
46+
if filters is not None:
47+
qs = apply_filters(filters, qs, info, None)
4248
return [deadline async for deadline in qs]
4349

4450
# Single ----------------------------

apps/project/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class ProjectType(UserResourceTypeMixin):
7878
contractor_id: strawberry.ID
7979

8080
name = string_field(Project.name)
81+
short_name = string_field(Project.short_name)
8182
description = string_field(Project.description)
8283

8384
@staticmethod

apps/track/queries.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import strawberry
44
import strawberry_django
5+
from django.db.models import IntegerField, Sum, Value
6+
from django.db.models.functions import Coalesce
57
from strawberry_django.filters import apply as apply_filters
68

79
from main.graphql.context import Info
@@ -11,7 +13,7 @@
1113
# from .enums import TimeEntryDateFilterEnum
1214
from .filters import ContractFilter, TaskFilter, TimeEntryFilter
1315
from .orders import ContractOrder, TaskOrder, TimeEntryOrder
14-
from .types import ContractType, TaskType, TimeEntryType
16+
from .types import ContractType, DailySummaryType, TaskType, TimeEntryType
1517

1618
# from django.db import models
1719

@@ -91,6 +93,79 @@ async def all_time_entries(
9193
raise Exception(f"Try using filters. To much data to return (Row count: {count})")
9294
return [time_entry async for time_entry in queryset]
9395

96+
@strawberry_django.field(
97+
description="Return total minutes and target minutes per day for the user within the given date range.",
98+
)
99+
async def daily_summary(
100+
self,
101+
info: Info,
102+
date_gte: datetime.date,
103+
date_lte: datetime.date,
104+
) -> list[DailySummaryType]:
105+
from apps.common.models import Event
106+
from apps.journal.models import Journal
107+
108+
from .models import TimeEntry
109+
110+
# Recorded minutes per date
111+
qs = (
112+
TimeEntry.objects.filter(
113+
user=info.context.request.user,
114+
date__gte=date_gte,
115+
date__lte=date_lte,
116+
)
117+
.values("date")
118+
.annotate(total_minutes=Coalesce(Sum("duration"), Value(0), output_field=IntegerField()))
119+
.order_by("date")
120+
)
121+
recorded_map: dict[datetime.date, int] = {}
122+
async for row in qs:
123+
recorded_map[row["date"]] = row["total_minutes"]
124+
125+
# Holiday / non-working event dates (cached)
126+
non_working_dates = set(await Event.aget_relative_event_dates())
127+
128+
# User journal entries (leave + wfh) in range
129+
journal_qs = Journal.objects.filter(
130+
user=info.context.request.user,
131+
date__gte=date_gte,
132+
date__lte=date_lte,
133+
).values("date", "leave_type", "wfh_type")
134+
journal_map: dict[datetime.date, dict] = {}
135+
async for row in journal_qs:
136+
journal_map[row["date"]] = row
137+
138+
# Build one entry per day in the range
139+
result: list[DailySummaryType] = []
140+
141+
total_days = (date_lte - date_gte).days + 1
142+
for offset in range(total_days):
143+
date = date_gte + datetime.timedelta(days=offset)
144+
journal = journal_map.get(date, {})
145+
leave_type = journal.get("leave_type")
146+
wfh_type = journal.get("wfh_type")
147+
is_weekend = Event.is_weekend(date)
148+
is_holiday = not is_weekend and date in non_working_dates
149+
150+
if is_weekend or is_holiday or leave_type == Journal.LeaveType.FULL:
151+
target = 0
152+
elif leave_type in (Journal.LeaveType.FIRST_HALF, Journal.LeaveType.SECOND_HALF):
153+
target = int(3.5 * 60)
154+
else:
155+
target = 7 * 60
156+
157+
result.append(
158+
DailySummaryType(
159+
date=date,
160+
total_minutes=recorded_map.get(date, 0),
161+
target_minutes=target,
162+
is_holiday=is_holiday,
163+
leave_type=leave_type,
164+
wfh_type=wfh_type,
165+
),
166+
)
167+
return result
168+
94169
# Single ----------------------------
95170
@strawberry_django.field
96171
async def contract(self, info: Info, pk: strawberry.ID) -> ContractType | None:

apps/track/types.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import datetime
2+
13
import strawberry
24
import strawberry_django
35
from django.db import models
46

57
from apps.common.types import ClientIdMixin, UserResourceTypeMixin
8+
from apps.journal.enums import JournalLeaveTypeEnum, JournalWorkFromHomeTypeEnum
69
from apps.project.types import ProjectType
710
from apps.user.types import UserType
811
from main.graphql.context import Info
@@ -13,6 +16,20 @@
1316
from .models import Contract, Task, TimeEntry
1417

1518

19+
@strawberry.type
20+
class DailySummaryType:
21+
date: datetime.date
22+
total_minutes: int
23+
target_minutes: int
24+
is_holiday: bool
25+
leave_type: JournalLeaveTypeEnum | None # type: ignore[reportInvalidTypeForm]
26+
wfh_type: JournalWorkFromHomeTypeEnum | None # type: ignore[reportInvalidTypeForm]
27+
28+
@strawberry.field
29+
def id(self) -> strawberry.ID:
30+
return strawberry.ID(self.date.isoformat())
31+
32+
1633
@strawberry_django.type(Contract)
1734
class ContractType(UserResourceTypeMixin):
1835
id: strawberry.ID

schema.graphql

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,16 @@ type DailyStandUpType {
184184
projectStat(pk: ID!): DailyStandUpProjectStatType
185185
}
186186

187+
type DailySummaryType {
188+
date: Date!
189+
totalMinutes: Int!
190+
targetMinutes: Int!
191+
isHoliday: Boolean!
192+
leaveType: JournalLeaveTypeEnum
193+
wfhType: JournalWorkFromHomeTypeEnum
194+
id: ID!
195+
}
196+
187197
"""Date (isoformat)"""
188198
scalar Date
189199

@@ -231,6 +241,16 @@ input DateRangeLookup {
231241
"""Date with time (isoformat)"""
232242
scalar DateTime
233243

244+
input DeadlineFilter {
245+
id: IDBaseFilterLookup
246+
endDate: DateDateFilterLookup
247+
isArchived: BoolBaseFilterLookup
248+
AND: DeadlineFilter
249+
OR: DeadlineFilter
250+
NOT: DeadlineFilter
251+
DISTINCT: Boolean
252+
}
253+
234254
type DeadlineType implements UserResourceTypeMixin {
235255
createdAt: DateTime!
236256
modifiedAt: DateTime!
@@ -265,11 +285,11 @@ input EventFilter {
265285
id: IDBaseFilterLookup
266286
startDate: DateDateFilterLookup
267287
endDate: DateDateFilterLookup
268-
types: [EventTypeEnum!]!
269288
AND: EventFilter
270289
OR: EventFilter
271290
NOT: EventFilter
272291
DISTINCT: Boolean
292+
types: [EventTypeEnum!]
273293
}
274294

275295
input EventOrder {
@@ -436,7 +456,7 @@ type PrivateQuery {
436456
contractors(filters: ContractorFilter, order: ContractorOrder, pagination: OffsetPaginationInput): ContractorTypeCountList!
437457
projects(filters: ProjectFilter, order: ProjectOrder, pagination: OffsetPaginationInput): ProjectTypeCountList!
438458
allProjects: [ProjectType!]!
439-
allDeadlines: [DeadlineType!]!
459+
allDeadlines(filters: DeadlineFilter = null): [DeadlineType!]!
440460
client(pk: ID!): ClientType
441461
contractor(pk: ID!): ContractorType
442462
project(pk: ID!): ProjectType
@@ -451,6 +471,11 @@ type PrivateQuery {
451471
allActiveTasks: [TaskType!]!
452472
myTimeEntries(date: Date!): [TimeEntryType!]!
453473
allTimeEntries(filters: TimeEntryFilter!): [TimeEntryType!]!
474+
475+
"""
476+
Return total minutes and target minutes per day for the user within the given date range.
477+
"""
478+
dailySummary(dateGte: Date!, dateLte: Date!): [DailySummaryType!]!
454479
contract(pk: ID!): ContractType
455480
task(pk: ID!): TaskType
456481
journal(date: Date!): JournalType
@@ -486,6 +511,7 @@ type ProjectType implements UserResourceTypeMixin {
486511
projectClientId: ID!
487512
contractorId: ID!
488513
name: String!
514+
shortName: String!
489515
description: String
490516
projectClient: ClientType!
491517
contractor: ContractorType!

0 commit comments

Comments
 (0)