diff --git a/apps/common/dataloaders.py b/apps/common/dataloaders.py index 62cb81b..eb02af1 100644 --- a/apps/common/dataloaders.py +++ b/apps/common/dataloaders.py @@ -1,10 +1,20 @@ import typing +# from asgiref.sync import sync_to_async from django.db import models +# import datetime + + +# from apps.common.models import Event +# from apps.journal.models import Journal +# from django.utils.functional import cached_property +# from strawberry.dataloader import DataLoader + DjangoModel = typing.TypeVar("DjangoModel", bound=models.Model) +# -- Helper def load_model_objects( Model: typing.Type[DjangoModel], keys: list[int], @@ -12,3 +22,49 @@ def load_model_objects( qs = Model.objects.filter(id__in=keys) _map = {obj.pk: obj for obj in qs} return [_map[key] for key in keys] + + +# -- Common models dataloaders + +# def load_user_last_working_date(keys: list[int]) -> list[datetime.date]: +# """ +# WITH recursive_days AS ( +# SELECT +# DATE_SUB(:input_date, INTERVAL 1 DAY) AS prev_day +# UNION ALL +# SELECT +# DATE_SUB(prev_day, INTERVAL 1 DAY) +# FROM recursive_days +# WHERE +# prev_day NOT IN ( +# SELECT +# {Event.date.db_column} +# FROM {Event._meta.db_table} +# WHERE {Event.type.db_column} IN ( +# {Event.Type.HOLIDAY.value}, +# {Event.Type.RETREAT.value} +# ) +# ) +# AND DAYOFWEEK(prev_day) NOT IN (1, 7) -- 1 = Sunday, 7 = Saturday +# AND ( +# SELECT COUNT(*) FROM recursive_days +# ) < %(NUMBER_OF_WORKING_DAYS_TO_SKIP + 1)s -- Stop after finding N working days +# ) +# SELECT MIN(prev_day) AS date_before_n_working_days +# FROM recursive_days; +# """ + +# recent_user_leave_dates_qs = ( +# Journal.as_leave_qs(recent_only=True) +# .filter(user__in=keys) +# .values_list('user', 'date') +# ) +# print(recent_user_leave_dates_qs) +# return load_model_objects(User, keys) + + +class CommonLoader: + # @cached_property + # def load_user_last_working_date(self): + # return DataLoader(load_fn=sync_to_async(load_user_last_working_date)) + ... diff --git a/apps/common/models.py b/apps/common/models.py index ae70976..79a80c6 100644 --- a/apps/common/models.py +++ b/apps/common/models.py @@ -1,6 +1,7 @@ import datetime import functools +from asgiref.sync import sync_to_async from django.db import models from django.utils import timezone @@ -68,20 +69,33 @@ def get_dates(self, include_weekends=False) -> list[datetime.date]: def get_last_working_date( cls, now_date: datetime.date, + skip_dates: list[datetime.date] | None = None, offset_count: int | None = None, ) -> datetime.date: # type: ignore[reportReturnType] # TODO: Add test - event_dates = set(cls.get_relative_event_dates()) + dates_to_skip = set(cls.get_relative_event_dates()) + if skip_dates: + dates_to_skip.update(skip_dates) found_count = 0 for x in range(30): # Create a 1 month window, Should be enough date = now_date - datetime.timedelta(days=x) - if cls.is_weekend(date) or date in event_dates: + if cls.is_weekend(date) or date in dates_to_skip: continue if offset_count is not None and found_count < offset_count: found_count += 1 continue return date + @classmethod + @sync_to_async + def aget_last_working_date( + cls, + now_date: datetime.date, + skip_dates: list[datetime.date] | None = None, + offset_count: int | None = None, + ) -> datetime.date: # type: ignore[reportReturnType] + return cls.get_last_working_date(now_date, skip_dates=skip_dates, offset_count=offset_count) + @classmethod def get_relative_events(cls) -> models.QuerySet["Event"]: """ @@ -94,7 +108,7 @@ def get_relative_events(cls) -> models.QuerySet["Event"]: return cls.objects.filter(start_date__gte=start_threshold, end_date__lte=end_threshold) @classmethod - @functools.cache + @functools.cache # TODO: URGENT! Clear this cache on events CUD def get_relative_event_dates(cls) -> list[datetime.date]: """ Return list of dates with holiday relative to current date diff --git a/apps/journal/dataloaders.py b/apps/journal/dataloaders.py index 8439181..85be6b4 100644 --- a/apps/journal/dataloaders.py +++ b/apps/journal/dataloaders.py @@ -1,6 +1,7 @@ import datetime from asgiref.sync import sync_to_async +from django.utils import timezone from django.utils.functional import cached_property from strawberry.dataloader import DataLoader @@ -39,6 +40,26 @@ def load_user_work_from_home(keys: list[tuple[int, datetime.date]]) -> list[Jour return [_map.get(key) for key in keys] +def load_user_leave_today(keys: list[int]) -> list[Journal.LeaveType | None]: + qs = Journal.objects.filter( + user__in=keys, + date=timezone.now().date(), + ).values_list("user_id", "leave_type") + + _map = {user_id: leave_type for user_id, leave_type in qs} + return [_map.get(key) for key in keys] + + +def load_user_work_from_home_today(keys: list[int]) -> list[Journal.WorkFromHomeType | None]: + qs = Journal.objects.filter( + user__in=keys, + date=timezone.now().date(), + ).values_list("user_id", "wfh_type") + + _map = {user_id: wfh_type for user_id, wfh_type in qs} + return [_map.get(key) for key in keys] + + class JournalDataLoader: @cached_property def load_user_leave(self): @@ -47,3 +68,11 @@ def load_user_leave(self): @cached_property def load_user_work_from_home(self): return DataLoader(load_fn=sync_to_async(load_user_work_from_home)) + + @cached_property + def load_user_leave_today(self): + return DataLoader(load_fn=sync_to_async(load_user_leave_today)) + + @cached_property + def load_user_work_from_home_today(self): + return DataLoader(load_fn=sync_to_async(load_user_work_from_home_today)) diff --git a/apps/journal/models.py b/apps/journal/models.py index 64396fa..87702ac 100644 --- a/apps/journal/models.py +++ b/apps/journal/models.py @@ -1,5 +1,8 @@ +import datetime + from django.core.exceptions import ValidationError from django.db import models +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from apps.user.models import User @@ -49,6 +52,21 @@ class Meta: # type: ignore[reportIncompatibleVariableOverride] models.Index(fields=["date"]), ] + @classmethod + def as_leave_qs(cls, recent_only=False) -> models.QuerySet["Journal"]: + """ + Return a Journal queryset with pre-applied leave filters + """ + qs = Journal.objects.filter( + leave_type__in=[ + Journal.LeaveType.FULL, + Journal.LeaveType.FIRST_HALF, + ], + ) + if recent_only: + return qs.filter(date__gte=timezone.now() - datetime.timedelta(days=30)) + return qs + def __str__(self): return f"{self.user_id}#{self.date}" diff --git a/apps/project/admin.py b/apps/project/admin.py index b936107..cb9167f 100644 --- a/apps/project/admin.py +++ b/apps/project/admin.py @@ -38,4 +38,4 @@ class ProjectAdmin(PreventDeleteAdminMixin, VersionAdmin, UserResourceAdmin): AutocompleteFilterFactory("Contractor", "contractor"), ) - list_display = ("name",) + list_display = ("name", "slide_order") diff --git a/apps/project/migrations/0006_project_slide_order.py b/apps/project/migrations/0006_project_slide_order.py new file mode 100644 index 0000000..b5b3e5e --- /dev/null +++ b/apps/project/migrations/0006_project_slide_order.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.15 on 2024-08-26 12:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("project", "0005_project_is_archived_deadline"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="slide_order", + field=models.PositiveSmallIntegerField(default=0, help_text="Used to order projects in daily stand-up slides"), + ), + ] diff --git a/apps/project/migrations/0007_project_logo_hd_alter_project_logo.py b/apps/project/migrations/0007_project_logo_hd_alter_project_logo.py new file mode 100644 index 0000000..ab8e6b7 --- /dev/null +++ b/apps/project/migrations/0007_project_logo_hd_alter_project_logo.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.15 on 2024-08-26 12:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("project", "0006_project_slide_order"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="logo_hd", + field=models.ImageField( + blank=True, help_text="Hight quality logo", max_length=255, null=True, upload_to="project/logo-hd/" + ), + ), + migrations.AlterField( + model_name="project", + name="logo", + field=models.ImageField( + blank=True, help_text="Low quality logo.", max_length=255, null=True, upload_to="project/logo/" + ), + ), + ] diff --git a/apps/project/models.py b/apps/project/models.py index 35a2aca..ba3980d 100644 --- a/apps/project/models.py +++ b/apps/project/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.utils.translation import gettext_lazy as _ from apps.common.models import UserResource @@ -20,8 +21,17 @@ def __str__(self): class Project(UserResource): name = models.CharField(max_length=225) description = models.TextField(blank=True) + # TODO: Validate image size for optimal performance logo = models.ImageField( upload_to="project/logo/", + help_text="Low quality logo.", + max_length=255, + blank=True, + null=True, + ) + logo_hd = models.ImageField( + upload_to="project/logo-hd/", + help_text="Hight quality logo", max_length=255, blank=True, null=True, @@ -32,6 +42,10 @@ class Project(UserResource): project_client = models.ForeignKey(Client, on_delete=models.PROTECT, related_name="projects") contractor = models.ForeignKey(Contractor, on_delete=models.PROTECT, related_name="projects") is_archived = models.BooleanField(default=False) + slide_order = models.PositiveSmallIntegerField( + default=0, + help_text=_("Used to order projects in daily stand-up slides"), + ) project_client_id: int contractor_id: int diff --git a/apps/project/queries.py b/apps/project/queries.py index eb0d71d..3bf5aff 100644 --- a/apps/project/queries.py +++ b/apps/project/queries.py @@ -33,7 +33,7 @@ class PrivateQuery: # Unbound ---------------------------- @strawberry_django.field async def all_projects(self, info: Info) -> list[ProjectType]: - qs = ProjectType.get_queryset(None, None, info).filter(is_archived=False).all() + qs = ProjectType.get_queryset(None, None, info).filter(is_archived=False).order_by("slide_order").all() return [project async for project in qs] @strawberry_django.field diff --git a/apps/project/types.py b/apps/project/types.py index 6192744..f35f5eb 100644 --- a/apps/project/types.py +++ b/apps/project/types.py @@ -67,6 +67,8 @@ async def remaining_days(self, root: strawberry.Parent[Deadline]) -> int: class ProjectType(UserResourceTypeMixin): id: strawberry.ID logo: strawberry.auto + logo_hd: strawberry.auto + slide_order: strawberry.auto project_client_id: strawberry.ID contractor_id: strawberry.ID diff --git a/apps/standup/types.py b/apps/standup/types.py index 9bbb66c..0dfc9f8 100644 --- a/apps/standup/types.py +++ b/apps/standup/types.py @@ -2,7 +2,6 @@ import strawberry import strawberry_django -from asgiref.sync import sync_to_async from django.db import models from apps.common.models import Event @@ -13,6 +12,7 @@ from apps.standup.models import Quote from apps.track.models import TimeEntry from apps.user.models import User +from apps.user.types import UserType from main.graphql.context import Info from utils.common import get_queryset_for_model from utils.strawberry.types import string_field @@ -35,18 +35,21 @@ class DailyStandUpProjectStatUserType: user_obj: strawberry.Private[User] date: strawberry.Private[datetime.date] - @strawberry.field - def id(self) -> strawberry.ID: - return strawberry.ID(str(self.user_obj.pk)) + id: strawberry.ID + last_active_date: datetime.date - @strawberry.field + @strawberry.field(deprecation_reason="Use user.display_picture instead") def display_picture(self) -> str | None: return self.user_obj.display_picture - @strawberry.field + @strawberry.field(deprecation_reason="Use user.display_name instead") def display_name(self) -> str: return self.user_obj.display_name + @strawberry.field + def user(self) -> UserType: + return self.user_obj # type: ignore[reportReturnType] + @strawberry.field async def leave(self, info: Info) -> JournalLeaveTypeEnum | None: # type: ignore[reportInvalidTypeForm] return await info.context.dl.journal.load_user_leave.load((self.user_obj.pk, self.date)) @@ -62,11 +65,11 @@ class DailyStandUpProjectStatType: date: strawberry.Private[datetime.date] async def _check_activity_from_date(self) -> datetime.date: - return await sync_to_async(Event.get_last_working_date)(now_date=self.date, offset_count=3) + return await Event.aget_last_working_date(now_date=self.date, offset_count=3) @strawberry.field async def last_working_date(self) -> datetime.date: - return await sync_to_async(Event.get_last_working_date)(now_date=self.date) + return await Event.aget_last_working_date(now_date=self.date) # XXX: For debugging only @strawberry.field @@ -79,18 +82,42 @@ def project(self) -> ProjectType: @strawberry.field async def users(self) -> list[DailyStandUpProjectStatUserType]: - last_working_date = await self._check_activity_from_date() + activity_from_date = await self._check_activity_from_date() time_entries_qs = ( TimeEntry.objects.filter( + ( + models.Q( + date__gte=activity_from_date, + date__lt=self.date, + status__in=[TimeEntry.Status.DOING, TimeEntry.Status.DONE], + ) + | models.Q(date=self.date) + ), task__contract__project=self.project_obj, - date__gte=last_working_date, ) + .order_by() .values("user") - .distinct() + .annotate( + active_date=models.Max("date"), + ) + .values_list("user", "active_date") ) + + time_entries_user_active_date_map = {user_id: active_date async for user_id, active_date in time_entries_qs} + + users_qs = User.objects.filter( + id__in=time_entries_user_active_date_map.keys(), + exclude_from_slides=False, + ).order_by("display_name") + return [ - DailyStandUpProjectStatUserType(user_obj=user, date=self.date) - async for user in User.objects.filter(id__in=time_entries_qs).all() + DailyStandUpProjectStatUserType( + user_obj=user, + date=self.date, + last_active_date=time_entries_user_active_date_map[user.pk], + id=strawberry.ID(f"{user.pk}-{self.project_obj.pk}"), + ) + async for user in users_qs.all() ] diff --git a/apps/track/filters.py b/apps/track/filters.py index fcbc16d..972d36a 100644 --- a/apps/track/filters.py +++ b/apps/track/filters.py @@ -36,7 +36,14 @@ class TimeEntryFilter: task: strawberry.auto date: strawberry.auto - types: list[TimeEntryTypeEnum] # type: ignore[reportInvalidTypeForm] + @strawberry_django.filter_field + def types( + self, + queryset: models.QuerySet, + value: list[TimeEntryTypeEnum], # type: ignore[reportInvalidTypeForm] + prefix: str, + ) -> tuple[models.QuerySet, models.Q]: + return queryset, models.Q(**{f"{prefix}type__in": value}) @strawberry_django.filter_field def project( diff --git a/apps/track/queries.py b/apps/track/queries.py index 009caec..8d614f7 100644 --- a/apps/track/queries.py +++ b/apps/track/queries.py @@ -2,6 +2,7 @@ import strawberry import strawberry_django +from strawberry_django.filters import apply as apply_filters from main.graphql.context import Info from utils.strawberry.paginations import CountList, pagination_field @@ -55,6 +56,19 @@ async def my_time_entries(self, info: Info, date: datetime.date) -> list[TimeEnt ) return [time_entry async for time_entry in qs] + @strawberry_django.field + async def all_time_entries( + self, + info: Info, + filters: TimeEntryFilter, + ) -> list[TimeEntryType]: + queryset = TimeEntryType.get_queryset(None, None, info) + queryset = apply_filters(filters, queryset, info, None) + count = await queryset.acount() + if count > 3000: # TODO: Is this fine? + raise Exception(f"Try using filters. To much data to return (Row count: {count})") + return [time_entry async for time_entry in queryset] + # Single ---------------------------- @strawberry_django.field async def contract(self, info: Info, pk: strawberry.ID) -> ContractType | None: diff --git a/apps/user/admin.py b/apps/user/admin.py index e6cd2ff..1e91972 100644 --- a/apps/user/admin.py +++ b/apps/user/admin.py @@ -30,6 +30,7 @@ class UserAdmin(DjangoUserAdmin): "last_name", "department", "display_picture", + "exclude_from_slides", ) }, ), diff --git a/apps/user/enums.py b/apps/user/enums.py index 052db48..3c76064 100644 --- a/apps/user/enums.py +++ b/apps/user/enums.py @@ -1 +1,10 @@ -enum_map = {} +import strawberry + +from utils.strawberry.enums import get_enum_name_from_django_field + +from .models import User + +UserDepartmentTypeEnum = strawberry.enum(User.Department, name="UserDepartmentTypeEnum") + + +enum_map = {get_enum_name_from_django_field(field): enum for field, enum in ((User.department, UserDepartmentTypeEnum),)} diff --git a/apps/user/migrations/0005_user_exclude_from_slides_alter_user_display_name.py b/apps/user/migrations/0005_user_exclude_from_slides_alter_user_display_name.py new file mode 100644 index 0000000..424a83e --- /dev/null +++ b/apps/user/migrations/0005_user_exclude_from_slides_alter_user_display_name.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.15 on 2024-08-26 12:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("user", "0004_user_display_picture_alter_user_department"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="exclude_from_slides", + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name="user", + name="display_name", + field=models.CharField(blank=True, max_length=255), + ), + ] diff --git a/apps/user/models.py b/apps/user/models.py index f9d08a1..c9ede65 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -23,13 +23,16 @@ class Department(models.IntegerChoices): email = models.EmailField(unique=True) invalid_email = models.BooleanField(default=False, help_text=_("Is Bounced email?")) display_name = models.CharField( - verbose_name=_("system generated user display name"), blank=True, max_length=255, ) display_picture = models.URLField(null=True, blank=True) department = models.PositiveSmallIntegerField(choices=Department.choices, null=True, blank=True) + # TODO: This is a hacky way to exclude useres from standup slides, for better integration implement + # support for custom teams with members & projects + exclude_from_slides = models.BooleanField(default=False) + objects: CustomUserManager = CustomUserManager() # type: ignore[reportAssignmentType] pk: int diff --git a/apps/user/queries.py b/apps/user/queries.py index 9a4b7da..1e7fdd5 100644 --- a/apps/user/queries.py +++ b/apps/user/queries.py @@ -1,29 +1,9 @@ import strawberry -import strawberry_django from asgiref.sync import sync_to_async from main.graphql.context import Info -from .models import User - - -@strawberry_django.type(User) -class UserType: - id: strawberry.ID - first_name: strawberry.auto - last_name: strawberry.auto - display_name: strawberry.auto - display_picture: strawberry.auto - - -@strawberry_django.type(User) -class UserMeType(UserType): - id: strawberry.ID - email: strawberry.auto - first_name: strawberry.auto - last_name: strawberry.auto - display_name: strawberry.auto - display_picture: strawberry.auto +from .types import UserMeType @strawberry.type diff --git a/apps/user/types.py b/apps/user/types.py index 7a25e54..f2cc346 100644 --- a/apps/user/types.py +++ b/apps/user/types.py @@ -1,15 +1,62 @@ +import datetime + import strawberry import strawberry_django +from django.utils import timezone + +from apps.common.models import Event +from apps.journal.enums import JournalLeaveTypeEnum, JournalWorkFromHomeTypeEnum +from apps.journal.models import Journal +from main.graphql.context import Info +from utils.strawberry.enums import enum_display_field, enum_field +from utils.strawberry.types import string_field from .models import User -@strawberry_django.type(User) -class UserType: +@strawberry.interface +class UserBaseType: + # NOTE: Can't use strawberry.auto on interface id: strawberry.ID - first_name: strawberry.auto - last_name: strawberry.auto + first_name = string_field(User.first_name) + last_name = string_field(User.last_name) + display_name = string_field(User.display_name) # type: ignore[reportArgumentType] + display_picture = string_field(User.display_picture) + + department = enum_field(User.department) + department_display = enum_display_field(User.department) + + @strawberry.field + async def leave_today( + self, + user: strawberry.Parent[User], + info: Info, + ) -> JournalLeaveTypeEnum | None: # type: ignore[reportInvalidTypeForm] + return await info.context.dl.journal.load_user_leave_today.load(user.pk) + + @strawberry.field + async def work_from_home_today( + self, + user: strawberry.Parent[User], + info: Info, + ) -> JournalWorkFromHomeTypeEnum | None: # type: ignore[reportInvalidTypeForm] + return await info.context.dl.journal.load_user_work_from_home_today.load(user.pk) + + +@strawberry_django.type(User) +class UserType(UserBaseType): ... + + +@strawberry_django.type(User) +class UserMeType(UserBaseType): + email: strawberry.auto + is_staff: strawberry.auto + is_superuser: strawberry.auto - @strawberry_django.field - def display_name(self, root: User) -> str: - return root.display_name + @strawberry.field + async def my_last_working_date(self) -> datetime.date: + recent_leaves_dates = list(Journal.as_leave_qs(recent_only=True).values_list("date", flat=True).distinct()) + return await Event.aget_last_working_date( + now_date=timezone.now().date(), + skip_dates=recent_leaves_dates, + ) diff --git a/schema.graphql b/schema.graphql index da29864..e0d19fc 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1,5 +1,6 @@ type AppEnumCollection { EventType: [AppEnumCollectionEventType!]! + UserDepartment: [AppEnumCollectionUserDepartment!]! TimeEntryType: [AppEnumCollectionTimeEntryType!]! TimeEntryStatus: [AppEnumCollectionTimeEntryStatus!]! JournalLeaveType: [AppEnumCollectionJournalLeaveType!]! @@ -31,6 +32,11 @@ type AppEnumCollectionTimeEntryType { label: String! } +type AppEnumCollectionUserDepartment { + key: UserDepartmentTypeEnum! + label: String! +} + input BoolBaseFilterLookup { """Exact match. Filter will be skipped on `null` value""" exact: Boolean @@ -160,8 +166,10 @@ type DailyStandUpProjectStatType { type DailyStandUpProjectStatUserType { id: ID! - displayPicture: String - displayName: String! + lastActiveDate: Date! + displayPicture: String @deprecated(reason: "Use user.display_picture instead") + displayName: String! @deprecated(reason: "Use user.display_name instead") + user: UserType! leave: JournalLeaveTypeEnum workFromHome: JournalWorkFromHomeTypeEnum } @@ -430,6 +438,7 @@ type PrivateQuery { """Return all UnArchived tasks""" allActiveTasks: [TaskType!]! myTimeEntries(date: Date!): [TimeEntryType!]! + allTimeEntries(filters: TimeEntryFilter!): [TimeEntryType!]! contract(pk: ID!): ContractType task(pk: ID!): TaskType journal(date: Date!): JournalType @@ -460,6 +469,8 @@ type ProjectType implements UserResourceTypeMixin { modifiedBy: UserType! id: ID! logo: DjangoImageType + logoHd: DjangoImageType + slideOrder: Int! projectClientId: ID! contractorId: ID! name: String! @@ -622,11 +633,11 @@ input TimeEntryFilter { user: DjangoModelFilterInput task: DjangoModelFilterInput date: DateDateFilterLookup - types: [TimeEntryTypeEnum!]! AND: TimeEntryFilter OR: TimeEntryFilter NOT: TimeEntryFilter DISTINCT: Boolean + types: [TimeEntryTypeEnum!] project: ID contract: ID } @@ -702,13 +713,41 @@ input TimeEntryUpdateInput { clientId: ID } -type UserMeType { +interface UserBaseType { id: ID! - firstName: String! - lastName: String! - displayName: String! + firstName: String + lastName: String + displayName: String displayPicture: String + department: UserDepartmentTypeEnum + departmentDisplay: String + leaveToday: JournalLeaveTypeEnum + workFromHomeToday: JournalWorkFromHomeTypeEnum +} + +enum UserDepartmentTypeEnum { + DATA_ANALYST + DESIGN + DEVELOPMENT + MANAGEMENT + PROJECT_MANAGER + QUALITY_ASSURANCE +} + +type UserMeType implements UserBaseType { + id: ID! + firstName: String + lastName: String + displayName: String + displayPicture: String + department: UserDepartmentTypeEnum + departmentDisplay: String + leaveToday: JournalLeaveTypeEnum + workFromHomeToday: JournalWorkFromHomeTypeEnum email: String! + isStaff: Boolean! + isSuperuser: Boolean! + myLastWorkingDate: Date! } type UserMeTypeMutationResponseType { @@ -724,9 +763,14 @@ interface UserResourceTypeMixin { modifiedBy: UserType! } -type UserType { +type UserType implements UserBaseType { id: ID! - firstName: String! - lastName: String! - displayName: String! + firstName: String + lastName: String + displayName: String + displayPicture: String + department: UserDepartmentTypeEnum + departmentDisplay: String + leaveToday: JournalLeaveTypeEnum + workFromHomeToday: JournalWorkFromHomeTypeEnum } \ No newline at end of file