From 5f2e8708ddbd7eeddc1a87b79365c4ebbb40b652 Mon Sep 17 00:00:00 2001 From: sudan45 Date: Wed, 27 Mar 2024 16:13:13 +0545 Subject: [PATCH 1/9] implement graphql query and mutation node for assignment --- apps/notification/mutation.py | 22 ++++++++++++-- apps/notification/schema.py | 55 +++++++++++++++++++++++++++++++++-- schema.graphql | 28 ++++++++++++++++++ 3 files changed, 100 insertions(+), 5 deletions(-) diff --git a/apps/notification/mutation.py b/apps/notification/mutation.py index 95e10311d0..514db2f1b5 100644 --- a/apps/notification/mutation.py +++ b/apps/notification/mutation.py @@ -2,12 +2,12 @@ import graphene -from utils.graphene.mutation import generate_input_type_for_serializer +from utils.graphene.mutation import PsGrapheneMutation, generate_input_type_for_serializer from utils.graphene.error_types import mutation_is_not_valid, CustomErrorType from .serializers import NotificationGqSerializer -from .schema import NotificationType -from .models import Notification +from .schema import AssignmentType, NotificationType +from .models import Assignment, Notification NotificationStatusInputType = generate_input_type_for_serializer( 'NotificationStatusInputType', @@ -44,5 +44,21 @@ def mutate(root, info, data): return NotificationStatusUpdate(result=instance, ok=True, errors=None) +class AssignmentIsDoneUpdate(PsGrapheneMutation): + class Arguments: + ids = graphene.List(graphene.NonNull(graphene.ID), required=True) + is_done = graphene.Boolean(required=True) + model = Assignment + result = graphene.List(graphene.NonNull(AssignmentType)) + permissions = [] + + @classmethod + def perform_mutate(cls, root, info, **kwargs): + instance = Assignment.objects.filter(id__in=kwargs['ids']) + instance.update(is_done=kwargs['is_done']) + return cls(result=instance, ok=True) + + class Mutation(object): notification_status_update = NotificationStatusUpdate.Field() + assignment_is_done_status_update = AssignmentIsDoneUpdate.Field() diff --git a/apps/notification/schema.py b/apps/notification/schema.py index e8b91ec4ec..d487c4f369 100644 --- a/apps/notification/schema.py +++ b/apps/notification/schema.py @@ -3,14 +3,19 @@ from django.db.models import QuerySet from graphene_django import DjangoObjectType from graphene_django_extras import DjangoObjectField, PageGraphqlPagination +from entry.schema import EntryType +from lead.schema import LeadDetailType +from project.schema import ProjectDetailType from utils.graphene.enums import EnumDescription from utils.graphene.types import CustomDjangoListObjectType from utils.graphene.fields import DjangoPaginatedListObjectField from deep.trackers import track_user -from .models import Notification -from .filter_set import NotificationGqlFilterSet +from lead.models import Lead +from entry.models import Entry +from .models import Assignment, Notification +from .filter_set import NotificationGqlFilterSet, AssignmentFilterSet from .enums import ( NotificationTypeEnum, NotificationStatusEnum, @@ -24,6 +29,13 @@ def get_user_notification_qs(info): ) +def get_user_assignment_qs(info): + track_user(info.context.request.user.profile) + return Assignment.objects.filter( + created_by=info.context.request.user + ) + + class NotificationType(DjangoObjectType): class Meta: model = Notification @@ -41,12 +53,45 @@ def get_custom_queryset(queryset, info, **kwargs): return get_user_notification_qs(info) +class AssignmentType(DjangoObjectType): + class Meta: + model = Assignment + + project = graphene.Field(ProjectDetailType) + content_type = graphene.String(required=False) + object_id = graphene.ID(required=True) + lead_type = graphene.Field(LeadDetailType) + entry_type = graphene.Field(EntryType) + + @staticmethod + def get_custom_queryset(queryset, info, **kwargs): + return get_user_assignment_qs(info) + + @staticmethod + def resolve_content_type(root, info): + return root.content_type.model + + @staticmethod + def resolve_lead_type(root, info): + return Lead(root.object_id) if root.content_type.model == 'lead' else None + + @staticmethod + def resolve_entry_type(root, info): + return Entry(root.object_id) if root.content_type.model == 'entry' else None + + class NotificationListType(CustomDjangoListObjectType): class Meta: model = Notification filterset_class = NotificationGqlFilterSet +class AssignmentListType(CustomDjangoListObjectType): + class Meta: + model = Assignment + filterset_class = AssignmentFilterSet + + class Query: notification = DjangoObjectField(NotificationType) notifications = DjangoPaginatedListObjectField( @@ -55,6 +100,12 @@ class Query: page_size_query_param='pageSize' ) ) + assignment = DjangoPaginatedListObjectField( + AssignmentListType, + pagination=PageGraphqlPagination( + page_size_query_param='pageSize' + ) + ) @staticmethod def resolve_notifications(root, info, **kwargs) -> QuerySet: diff --git a/schema.graphql b/schema.graphql index c8fe6ed00e..9a2737fb43 100644 --- a/schema.graphql +++ b/schema.graphql @@ -3081,6 +3081,32 @@ type AssessmentType { modifiedBy: UserType } +type AssignmentIsDoneUpdate { + errors: [GenericScalar!] + ok: Boolean + result: [AssignmentType!] +} + +type AssignmentListType { + results: [AssignmentType!] + totalCount: Int + page: Int + pageSize: Int +} + +type AssignmentType { + id: ID! + createdAt: DateTime! + createdBy: UserType + createdFor: UserType! + project: ProjectDetailType + isDone: Boolean! + contentType: String + objectId: ID! + leadType: LeadDetailType + entryType: EntryType +} + type AssistedTaggingModelPredictionTagType { id: ID! name: String! @@ -4914,6 +4940,7 @@ type Mutation { genericExportCreate(data: GenericExportCreateInputType!): CreateUserGenericExport genericExportCancel(id: ID!): CancelUserGenericExport notificationStatusUpdate(data: NotificationStatusInputType!): NotificationStatusUpdate + assignmentIsDoneStatusUpdate(ids: [ID!]!, isDone: Boolean!): AssignmentIsDoneUpdate projectCreate(data: ProjectCreateInputType!): CreateProject joinProject(data: ProjectJoinRequestInputType!): CreateProjectJoin projectJoinRequestDelete(projectId: ID!): ProjectJoinRequestDelete @@ -5678,6 +5705,7 @@ type Query { publicLead(uuid: UUID!): PublicLeadMetaType notification(id: ID!): NotificationType notifications(timestamp: DateTime, timestampLte: DateTime, timestampGte: DateTime, isPending: Boolean, notificationType: NotificationTypeEnum, status: NotificationStatusEnum, page: Int = 1, ordering: String, pageSize: Int): NotificationListType + assignment(project: ID, isDone: Boolean, page: Int = 1, ordering: String, pageSize: Int): AssignmentListType region(id: ID!): RegionDetailType regions(id: Float, code: String, title: String, public: Boolean, project: [ID], createdAt: DateTime, createdBy: [ID], modifiedAt: DateTime, modifiedBy: [ID], createdAt_Lt: Date, createdAt_Gte: Date, modifiedAt_Lt: Date, modifiedAt_Gt: Date, excludeProject: [ID!], search: String, page: Int = 1, ordering: String, pageSize: Int): RegionListType organization(id: ID!): OrganizationType From acc7fcaa3e9185b943f2d52402ef376c9e9155ce Mon Sep 17 00:00:00 2001 From: sudan45 Date: Tue, 2 Apr 2024 11:42:18 +0545 Subject: [PATCH 2/9] implemented bulk update of assignmnet of users --- apps/notification/mutation.py | 10 +++++++--- schema.graphql | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/notification/mutation.py b/apps/notification/mutation.py index 514db2f1b5..a833fa08f2 100644 --- a/apps/notification/mutation.py +++ b/apps/notification/mutation.py @@ -46,7 +46,7 @@ def mutate(root, info, data): class AssignmentIsDoneUpdate(PsGrapheneMutation): class Arguments: - ids = graphene.List(graphene.NonNull(graphene.ID), required=True) + id = graphene.ID(required=True) is_done = graphene.Boolean(required=True) model = Assignment result = graphene.List(graphene.NonNull(AssignmentType)) @@ -54,11 +54,15 @@ class Arguments: @classmethod def perform_mutate(cls, root, info, **kwargs): - instance = Assignment.objects.filter(id__in=kwargs['ids']) + if kwargs['id'] == '': # empty string update all assignment of the user in bulk + instance = cls.model.objects.filter(is_done=False, created_for=info.context.user) + instance.update(is_done=kwargs['is_done']) + return cls(result=instance, ok=True) + instance = cls.model.objects.filter(id=kwargs['id']) instance.update(is_done=kwargs['is_done']) return cls(result=instance, ok=True) class Mutation(object): notification_status_update = NotificationStatusUpdate.Field() - assignment_is_done_status_update = AssignmentIsDoneUpdate.Field() + assignment_status_update = AssignmentIsDoneUpdate.Field() diff --git a/schema.graphql b/schema.graphql index 9a2737fb43..1bb11affd8 100644 --- a/schema.graphql +++ b/schema.graphql @@ -4940,7 +4940,7 @@ type Mutation { genericExportCreate(data: GenericExportCreateInputType!): CreateUserGenericExport genericExportCancel(id: ID!): CancelUserGenericExport notificationStatusUpdate(data: NotificationStatusInputType!): NotificationStatusUpdate - assignmentIsDoneStatusUpdate(ids: [ID!]!, isDone: Boolean!): AssignmentIsDoneUpdate + assignmentStatusUpdate(id: ID!, isDone: Boolean!): AssignmentIsDoneUpdate projectCreate(data: ProjectCreateInputType!): CreateProject joinProject(data: ProjectJoinRequestInputType!): CreateProjectJoin projectJoinRequestDelete(projectId: ID!): ProjectJoinRequestDelete From a1f251f6206c98edbf577905f6a2043667c42ecd Mon Sep 17 00:00:00 2001 From: sudan45 Date: Wed, 3 Apr 2024 10:06:10 +0545 Subject: [PATCH 3/9] implemnt bulk status update of asssignment --- apps/notification/mutation.py | 33 ++++++++++++++++++++++++--------- apps/notification/schema.py | 6 ++---- schema.graphql | 21 ++++++++++++++------- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/apps/notification/mutation.py b/apps/notification/mutation.py index a833fa08f2..a5a2028554 100644 --- a/apps/notification/mutation.py +++ b/apps/notification/mutation.py @@ -2,10 +2,10 @@ import graphene -from utils.graphene.mutation import PsGrapheneMutation, generate_input_type_for_serializer +from utils.graphene.mutation import GrapheneMutation, PsGrapheneMutation, generate_input_type_for_serializer from utils.graphene.error_types import mutation_is_not_valid, CustomErrorType -from .serializers import NotificationGqSerializer +from .serializers import AssignmentSerializer, NotificationGqSerializer from .schema import AssignmentType, NotificationType from .models import Assignment, Notification @@ -15,6 +15,11 @@ ) +AssignmentInputType = generate_input_type_for_serializer( + 'AssignmentInputType', + serializer_class=AssignmentSerializer +) + class NotificationStatusUpdate(graphene.Mutation): class Arguments: @@ -44,9 +49,22 @@ def mutate(root, info, data): return NotificationStatusUpdate(result=instance, ok=True, errors=None) -class AssignmentIsDoneUpdate(PsGrapheneMutation): +class AssignmentStatusUpdate(GrapheneMutation): class Arguments: id = graphene.ID(required=True) + data = AssignmentInputType(required=True) + model = Assignment + result = graphene.Field(AssignmentType) + serializer_class = AssignmentSerializer + permissions = [] + + @classmethod + def check_permissions(cls, info, **kwargs): + return True # global permissions is always True + + +class AsssignmentBulkStatusUpdate(PsGrapheneMutation): + class Arguments: is_done = graphene.Boolean(required=True) model = Assignment result = graphene.List(graphene.NonNull(AssignmentType)) @@ -54,15 +72,12 @@ class Arguments: @classmethod def perform_mutate(cls, root, info, **kwargs): - if kwargs['id'] == '': # empty string update all assignment of the user in bulk - instance = cls.model.objects.filter(is_done=False, created_for=info.context.user) - instance.update(is_done=kwargs['is_done']) - return cls(result=instance, ok=True) - instance = cls.model.objects.filter(id=kwargs['id']) + instance = cls.model.objects.filter(created_for=info.context.user, is_done=False) instance.update(is_done=kwargs['is_done']) return cls(result=instance, ok=True) class Mutation(object): notification_status_update = NotificationStatusUpdate.Field() - assignment_status_update = AssignmentIsDoneUpdate.Field() + assignment_status_update = AssignmentStatusUpdate.Field() + assignment_bulk_status_update = AsssignmentBulkStatusUpdate.Field() diff --git a/apps/notification/schema.py b/apps/notification/schema.py index d487c4f369..68138b216e 100644 --- a/apps/notification/schema.py +++ b/apps/notification/schema.py @@ -31,9 +31,7 @@ def get_user_notification_qs(info): def get_user_assignment_qs(info): track_user(info.context.request.user.profile) - return Assignment.objects.filter( - created_by=info.context.request.user - ) + return Assignment.get_for(track_user) class NotificationType(DjangoObjectType): @@ -100,7 +98,7 @@ class Query: page_size_query_param='pageSize' ) ) - assignment = DjangoPaginatedListObjectField( + assignments = DjangoPaginatedListObjectField( AssignmentListType, pagination=PageGraphqlPagination( page_size_query_param='pageSize' diff --git a/schema.graphql b/schema.graphql index 1bb11affd8..da06b1bfa9 100644 --- a/schema.graphql +++ b/schema.graphql @@ -3081,12 +3081,6 @@ type AssessmentType { modifiedBy: UserType } -type AssignmentIsDoneUpdate { - errors: [GenericScalar!] - ok: Boolean - result: [AssignmentType!] -} - type AssignmentListType { results: [AssignmentType!] totalCount: Int @@ -3094,6 +3088,12 @@ type AssignmentListType { pageSize: Int } +type AssignmentStatusUpdate { + errors: [GenericScalar!] + ok: Boolean + result: AssignmentType +} + type AssignmentType { id: ID! createdAt: DateTime! @@ -3171,6 +3171,12 @@ type AssistedTaggingRootQueryType { predictionTags: [AssistedTaggingModelPredictionTagType!] } +type AsssignmentBulkStatusUpdate { + errors: [GenericScalar!] + ok: Boolean + result: [AssignmentType!] +} + type AtomFeedFieldType { key: String label: String @@ -4940,7 +4946,8 @@ type Mutation { genericExportCreate(data: GenericExportCreateInputType!): CreateUserGenericExport genericExportCancel(id: ID!): CancelUserGenericExport notificationStatusUpdate(data: NotificationStatusInputType!): NotificationStatusUpdate - assignmentStatusUpdate(id: ID!, isDone: Boolean!): AssignmentIsDoneUpdate + assignmentStatusUpdate(id: ID!, isDone: Boolean!): AssignmentStatusUpdate + assignmentBulkStatusUpdate(isDone: Boolean!): AsssignmentBulkStatusUpdate projectCreate(data: ProjectCreateInputType!): CreateProject joinProject(data: ProjectJoinRequestInputType!): CreateProjectJoin projectJoinRequestDelete(projectId: ID!): ProjectJoinRequestDelete From 8049adafc36bf266573c2bba3daf32e4ea139f0f Mon Sep 17 00:00:00 2001 From: sudan45 Date: Wed, 10 Apr 2024 15:35:51 +0545 Subject: [PATCH 4/9] - implement dataloaders in assignment - update mutation of assignment --- apps/notification/dataloaders.py | 52 ++++++++++++++++++++++++++++++++ apps/notification/schema.py | 32 ++++++-------------- deep/dataloaders.py | 4 +++ schema.graphql | 20 ++++++++---- 4 files changed, 80 insertions(+), 28 deletions(-) create mode 100644 apps/notification/dataloaders.py diff --git a/apps/notification/dataloaders.py b/apps/notification/dataloaders.py new file mode 100644 index 0000000000..915d37f923 --- /dev/null +++ b/apps/notification/dataloaders.py @@ -0,0 +1,52 @@ +from django.utils.functional import cached_property + +from promise import Promise + +from entry.models import Entry +from notification.models import Assignment +from lead.models import Lead + +from utils.graphene.dataloaders import DataLoaderWithContext, WithContextMixin + + +class AssignmentLoader(DataLoaderWithContext): + def batch_load_fn(self, keys): + assignment_qs = list( + Assignment.objects + .filter(id__in=keys) + .values_list('id', 'content_type__app_label', 'object_id') + ) + + leads_id = [] + entries_id = [] + for _, content_type, object_id in assignment_qs: + if content_type == 'entry': + entries_id.append(object_id) + elif content_type == 'lead': + leads_id.append(object_id) + + lead_id_map = { + lead.id: lead + for lead in Lead.objects.filter(id__in=leads_id) + } if leads_id else {} + + entry_id_map = { + entry.id: entry + for entry in Entry.objects.filter(id__in=entries_id) + } if entries_id else {} + + _result = { + _id: { + 'content_type': content_type, + 'lead': lead_id_map.get(object_id) if content_type == 'lead' else None, + 'entry': entry_id_map.get(object_id) if content_type == 'entry' else None, + } + for _id, content_type, object_id in assignment_qs + } + return Promise.resolve([_result[key] for key in keys]) + + +class DataLoaders(WithContextMixin): + @cached_property + def assignment(self): + return AssignmentLoader(context=self.context) diff --git a/apps/notification/schema.py b/apps/notification/schema.py index 68138b216e..ecfd861d2a 100644 --- a/apps/notification/schema.py +++ b/apps/notification/schema.py @@ -12,8 +12,6 @@ from utils.graphene.fields import DjangoPaginatedListObjectField from deep.trackers import track_user -from lead.models import Lead -from entry.models import Entry from .models import Assignment, Notification from .filter_set import NotificationGqlFilterSet, AssignmentFilterSet from .enums import ( @@ -51,31 +49,21 @@ def get_custom_queryset(queryset, info, **kwargs): return get_user_notification_qs(info) +class AssignmentContentDataType(graphene.ObjectType): + content_type = graphene.String(required=False) + lead = graphene.Field(LeadDetailType) + entry = graphene.Field(EntryType) + + class AssignmentType(DjangoObjectType): class Meta: model = Assignment - + id = graphene.ID(required=True) project = graphene.Field(ProjectDetailType) - content_type = graphene.String(required=False) - object_id = graphene.ID(required=True) - lead_type = graphene.Field(LeadDetailType) - entry_type = graphene.Field(EntryType) + content_data = graphene.Field(AssignmentContentDataType) - @staticmethod - def get_custom_queryset(queryset, info, **kwargs): - return get_user_assignment_qs(info) - - @staticmethod - def resolve_content_type(root, info): - return root.content_type.model - - @staticmethod - def resolve_lead_type(root, info): - return Lead(root.object_id) if root.content_type.model == 'lead' else None - - @staticmethod - def resolve_entry_type(root, info): - return Entry(root.object_id) if root.content_type.model == 'entry' else None + def resolve_content_data(root, info): + return info.context.dl.notification.assignment.load(root.pk) class NotificationListType(CustomDjangoListObjectType): diff --git a/deep/dataloaders.py b/deep/dataloaders.py index 3e18952205..61ca9f6890 100644 --- a/deep/dataloaders.py +++ b/deep/dataloaders.py @@ -16,6 +16,7 @@ from gallery.dataloaders import DataLoaders as DeepGalleryDataLoaders from assessment_registry.dataloaders import DataLoaders as AssessmentRegistryDataLoaders from assisted_tagging.dataloaders import DataLoaders as AssistedTaggingLoaders +from notification.dataloaders import DataLoaders as AssignmentLoaders class GlobalDataLoaders(WithContextMixin): @@ -74,3 +75,6 @@ def deep_gallery(self): @cached_property def assisted_tagging(self): return AssistedTaggingLoaders(context=self.context) + + def notification(self): + return AssignmentLoaders(context=self.context) diff --git a/schema.graphql b/schema.graphql index da06b1bfa9..2eb8e25e10 100644 --- a/schema.graphql +++ b/schema.graphql @@ -3081,6 +3081,16 @@ type AssessmentType { modifiedBy: UserType } +type AssignmentContentDataType { + contentType: String + lead: LeadDetailType + entry: EntryType +} + +input AssignmentInputType { + isDone: Boolean +} + type AssignmentListType { results: [AssignmentType!] totalCount: Int @@ -3101,10 +3111,8 @@ type AssignmentType { createdFor: UserType! project: ProjectDetailType isDone: Boolean! - contentType: String - objectId: ID! - leadType: LeadDetailType - entryType: EntryType + objectId: Int! + contentData: AssignmentContentDataType } type AssistedTaggingModelPredictionTagType { @@ -4946,7 +4954,7 @@ type Mutation { genericExportCreate(data: GenericExportCreateInputType!): CreateUserGenericExport genericExportCancel(id: ID!): CancelUserGenericExport notificationStatusUpdate(data: NotificationStatusInputType!): NotificationStatusUpdate - assignmentStatusUpdate(id: ID!, isDone: Boolean!): AssignmentStatusUpdate + assignmentStatusUpdate(data: AssignmentInputType!, id: ID!): AssignmentStatusUpdate assignmentBulkStatusUpdate(isDone: Boolean!): AsssignmentBulkStatusUpdate projectCreate(data: ProjectCreateInputType!): CreateProject joinProject(data: ProjectJoinRequestInputType!): CreateProjectJoin @@ -5712,7 +5720,7 @@ type Query { publicLead(uuid: UUID!): PublicLeadMetaType notification(id: ID!): NotificationType notifications(timestamp: DateTime, timestampLte: DateTime, timestampGte: DateTime, isPending: Boolean, notificationType: NotificationTypeEnum, status: NotificationStatusEnum, page: Int = 1, ordering: String, pageSize: Int): NotificationListType - assignment(project: ID, isDone: Boolean, page: Int = 1, ordering: String, pageSize: Int): AssignmentListType + assignments(project: ID, isDone: Boolean, page: Int = 1, ordering: String, pageSize: Int): AssignmentListType region(id: ID!): RegionDetailType regions(id: Float, code: String, title: String, public: Boolean, project: [ID], createdAt: DateTime, createdBy: [ID], modifiedAt: DateTime, modifiedBy: [ID], createdAt_Lt: Date, createdAt_Gte: Date, modifiedAt_Lt: Date, modifiedAt_Gt: Date, excludeProject: [ID!], search: String, page: Int = 1, ordering: String, pageSize: Int): RegionListType organization(id: ID!): OrganizationType From 846e88e7b0648103ccbd56ee8a65c667a398aad2 Mon Sep 17 00:00:00 2001 From: sudan45 Date: Fri, 12 Apr 2024 10:58:04 +0545 Subject: [PATCH 5/9] - dataloader updated - assignment bulk update --- apps/notification/mutation.py | 30 +++++++++++++++++------------- apps/notification/schema.py | 28 ++++++++++++++++++++++------ schema.graphql | 35 ++++++++++++++++++++++++----------- 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/apps/notification/mutation.py b/apps/notification/mutation.py index a5a2028554..cc10729b54 100644 --- a/apps/notification/mutation.py +++ b/apps/notification/mutation.py @@ -2,7 +2,7 @@ import graphene -from utils.graphene.mutation import GrapheneMutation, PsGrapheneMutation, generate_input_type_for_serializer +from utils.graphene.mutation import GrapheneMutation, generate_input_type_for_serializer from utils.graphene.error_types import mutation_is_not_valid, CustomErrorType from .serializers import AssignmentSerializer, NotificationGqSerializer @@ -49,35 +49,39 @@ def mutate(root, info, data): return NotificationStatusUpdate(result=instance, ok=True, errors=None) -class AssignmentStatusUpdate(GrapheneMutation): +class AssignmentUpdate(GrapheneMutation): class Arguments: id = graphene.ID(required=True) data = AssignmentInputType(required=True) model = Assignment result = graphene.Field(AssignmentType) serializer_class = AssignmentSerializer - permissions = [] @classmethod def check_permissions(cls, info, **kwargs): return True # global permissions is always True + @classmethod + def filter_queryset(cls, qs, info): + return Assignment.get_for(info.context.user) -class AsssignmentBulkStatusUpdate(PsGrapheneMutation): - class Arguments: - is_done = graphene.Boolean(required=True) + +class AsssignmentBulkStatusMarkAsDone(GrapheneMutation): model = Assignment - result = graphene.List(graphene.NonNull(AssignmentType)) - permissions = [] + serializer_class = AssignmentSerializer + + @classmethod + def check_permissions(cls, info, **kwargs): + return True @classmethod def perform_mutate(cls, root, info, **kwargs): - instance = cls.model.objects.filter(created_for=info.context.user, is_done=False) - instance.update(is_done=kwargs['is_done']) - return cls(result=instance, ok=True) + instance = Assignment.get_for(info.context.user).filter(is_done=False) + instance.update(is_done=True) + return cls(ok=True) class Mutation(object): notification_status_update = NotificationStatusUpdate.Field() - assignment_status_update = AssignmentStatusUpdate.Field() - assignment_bulk_status_update = AsssignmentBulkStatusUpdate.Field() + assignment_update = AssignmentUpdate.Field() + assignment_bulk_status_mark_as_done = AsssignmentBulkStatusMarkAsDone.Field() diff --git a/apps/notification/schema.py b/apps/notification/schema.py index ecfd861d2a..9145764fae 100644 --- a/apps/notification/schema.py +++ b/apps/notification/schema.py @@ -3,9 +3,7 @@ from django.db.models import QuerySet from graphene_django import DjangoObjectType from graphene_django_extras import DjangoObjectField, PageGraphqlPagination -from entry.schema import EntryType -from lead.schema import LeadDetailType -from project.schema import ProjectDetailType +from lead.models import Lead from utils.graphene.enums import EnumDescription from utils.graphene.types import CustomDjangoListObjectType @@ -49,17 +47,35 @@ def get_custom_queryset(queryset, info, **kwargs): return get_user_notification_qs(info) +class AssignmentLeadDetailType(graphene.ObjectType): + id = graphene.ID(required=True) + title = graphene.String(required=True) + + class Meta: + model = Lead + fields = ['id', 'title'] + + +class AssignmentEntryDetailType(graphene.ObjectType): + id = graphene.ID(required=True) + + +class AssignmentProjectDetailType(graphene.ObjectType): + id = graphene.ID(required=True) + title = graphene.String(required=True) + + class AssignmentContentDataType(graphene.ObjectType): content_type = graphene.String(required=False) - lead = graphene.Field(LeadDetailType) - entry = graphene.Field(EntryType) + lead = graphene.Field(AssignmentLeadDetailType) + entry = graphene.Field(AssignmentEntryDetailType) class AssignmentType(DjangoObjectType): class Meta: model = Assignment id = graphene.ID(required=True) - project = graphene.Field(ProjectDetailType) + project = graphene.Field(AssignmentProjectDetailType) content_data = graphene.Field(AssignmentContentDataType) def resolve_content_data(root, info): diff --git a/schema.graphql b/schema.graphql index 2eb8e25e10..259f6fd92c 100644 --- a/schema.graphql +++ b/schema.graphql @@ -3083,14 +3083,23 @@ type AssessmentType { type AssignmentContentDataType { contentType: String - lead: LeadDetailType - entry: EntryType + lead: AssignmentLeadDetailType + entry: AssignmentEntryDetailType +} + +type AssignmentEntryDetailType { + id: ID! } input AssignmentInputType { isDone: Boolean } +type AssignmentLeadDetailType { + id: ID! + title: String! +} + type AssignmentListType { results: [AssignmentType!] totalCount: Int @@ -3098,10 +3107,9 @@ type AssignmentListType { pageSize: Int } -type AssignmentStatusUpdate { - errors: [GenericScalar!] - ok: Boolean - result: AssignmentType +type AssignmentProjectDetailType { + id: ID! + title: String! } type AssignmentType { @@ -3109,12 +3117,18 @@ type AssignmentType { createdAt: DateTime! createdBy: UserType createdFor: UserType! - project: ProjectDetailType + project: AssignmentProjectDetailType isDone: Boolean! objectId: Int! contentData: AssignmentContentDataType } +type AssignmentUpdate { + errors: [GenericScalar!] + ok: Boolean + result: AssignmentType +} + type AssistedTaggingModelPredictionTagType { id: ID! name: String! @@ -3179,10 +3193,9 @@ type AssistedTaggingRootQueryType { predictionTags: [AssistedTaggingModelPredictionTagType!] } -type AsssignmentBulkStatusUpdate { +type AsssignmentBulkStatusMarkAsDone { errors: [GenericScalar!] ok: Boolean - result: [AssignmentType!] } type AtomFeedFieldType { @@ -4954,8 +4967,8 @@ type Mutation { genericExportCreate(data: GenericExportCreateInputType!): CreateUserGenericExport genericExportCancel(id: ID!): CancelUserGenericExport notificationStatusUpdate(data: NotificationStatusInputType!): NotificationStatusUpdate - assignmentStatusUpdate(data: AssignmentInputType!, id: ID!): AssignmentStatusUpdate - assignmentBulkStatusUpdate(isDone: Boolean!): AsssignmentBulkStatusUpdate + assignmentUpdate(data: AssignmentInputType!, id: ID!): AssignmentUpdate + assignmentBulkStatusMarkAsDone: AsssignmentBulkStatusMarkAsDone projectCreate(data: ProjectCreateInputType!): CreateProject joinProject(data: ProjectJoinRequestInputType!): CreateProjectJoin projectJoinRequestDelete(projectId: ID!): ProjectJoinRequestDelete From 693a83b2eab639b4368d07bd4c6b212659532694 Mon Sep 17 00:00:00 2001 From: sudan45 Date: Fri, 12 Apr 2024 16:41:31 +0545 Subject: [PATCH 6/9] add testcase of assignment schema --- apps/notification/factories.py | 6 ++++ apps/notification/tests/test_schemas.py | 40 ++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/apps/notification/factories.py b/apps/notification/factories.py index 492aa68c4a..93f884ac23 100644 --- a/apps/notification/factories.py +++ b/apps/notification/factories.py @@ -1,6 +1,7 @@ from factory.django import DjangoModelFactory from notification.models import ( + Assignment, Notification, ) @@ -8,3 +9,8 @@ class NotificationFactory(DjangoModelFactory): class Meta: model = Notification + + +class AssignmentFactory(DjangoModelFactory): + class Meta: + model = Assignment diff --git a/apps/notification/tests/test_schemas.py b/apps/notification/tests/test_schemas.py index ece686a6fa..29195196a3 100644 --- a/apps/notification/tests/test_schemas.py +++ b/apps/notification/tests/test_schemas.py @@ -6,7 +6,8 @@ from project.factories import ProjectFactory from notification.models import Notification -from notification.factories import NotificationFactory +from notification.factories import AssignmentFactory, NotificationFactory +from lead.factories import LeadFactory class TestNotificationQuerySchema(GraphQLTestCase): @@ -186,3 +187,40 @@ def _query_check(filters, **kwargs): content = _query_check(filters) self.assertEqual(content['data']['notifications']['totalCount'], count, f'\n{filters=} \n{content=}') self.assertEqual(len(content['data']['notifications']['results']), count, f'\n{filters=} \n{content=}') + + +class TestAssignmentQuerySchema(GraphQLTestCase): + def test_assignments_query(self): + query = ''' + query MyQuery { + assignments(isDone: false) { + results { + contentData { + contentType + entry { + id + } + lead { + id + title + } + } + createdAt + id + isDone + objectId + project { + id + title + } + } + } + } + ''' + project = ProjectFactory.create() + user = UserFactory.create() + lead = LeadFactory.create() + assignment = AssignmentFactory.create( + project=project.id, + object_id=lead.id + ) From 5522557be6e88ffad9b54e078de375d9f7e4ac82 Mon Sep 17 00:00:00 2001 From: sudan45 Date: Mon, 15 Apr 2024 17:23:10 +0545 Subject: [PATCH 7/9] - update enum for content types - optimize the dataloaders --- apps/notification/dataloaders.py | 51 ++++++++++++++++++++++---------- apps/notification/enums.py | 10 ++++++- apps/notification/models.py | 5 +++- apps/notification/schema.py | 17 +++++------ 4 files changed, 56 insertions(+), 27 deletions(-) diff --git a/apps/notification/dataloaders.py b/apps/notification/dataloaders.py index 915d37f923..c16b64ad63 100644 --- a/apps/notification/dataloaders.py +++ b/apps/notification/dataloaders.py @@ -2,9 +2,10 @@ from promise import Promise -from entry.models import Entry +from entry.models import EntryComment from notification.models import Assignment from lead.models import Lead +from quality_assurance.models import EntryReviewComment from utils.graphene.dataloaders import DataLoaderWithContext, WithContextMixin @@ -14,32 +15,50 @@ def batch_load_fn(self, keys): assignment_qs = list( Assignment.objects .filter(id__in=keys) - .values_list('id', 'content_type__app_label', 'object_id') + .values_list('id', 'content_type__model', 'object_id') ) leads_id = [] - entries_id = [] + entry_review_comment_id = [] + for _, content_type, object_id in assignment_qs: - if content_type == 'entry': - entries_id.append(object_id) - elif content_type == 'lead': + # TODO use dict map function + if content_type == Lead._meta.model_name: leads_id.append(object_id) + elif content_type == EntryReviewComment._meta.model_name: + entry_review_comment_id.append(object_id) + + _lead_id_map = {} + + for _id, title in Lead.objects.filter(id__in=leads_id).values_list('id', 'title'): + _lead_id_map[_id] = dict( + id=_id, + title=title + ) - lead_id_map = { - lead.id: lead - for lead in Lead.objects.filter(id__in=leads_id) - } if leads_id else {} + _entry_review_comment_id_map = {} - entry_id_map = { - entry.id: entry - for entry in Entry.objects.filter(id__in=entries_id) - } if entries_id else {} + for _id, text, entry_id, entry_excerpt, lead_id in EntryReviewComment.objects.filter( + id__in=entry_review_comment_id).values_list( + 'id', + 'comment_texts__text', + 'entry__id', + 'entry__excerpt', + 'entry__lead_id' + ): + _entry_review_comment_id_map[_id] = dict( + id=_id, + text=text, + entry_id=entry_id, + entry_excerpt=entry_excerpt, + lead_id=lead_id + ) _result = { _id: { 'content_type': content_type, - 'lead': lead_id_map.get(object_id) if content_type == 'lead' else None, - 'entry': entry_id_map.get(object_id) if content_type == 'entry' else None, + 'lead': _lead_id_map.get(object_id) if content_type == 'lead' else None, + 'entry_review_comment': _entry_review_comment_id_map.get(object_id) if content_type == 'entryreviewcomment' else None, } for _id, content_type, object_id in assignment_qs } diff --git a/apps/notification/enums.py b/apps/notification/enums.py index 36cff9e0dd..83dcf2b9ec 100644 --- a/apps/notification/enums.py +++ b/apps/notification/enums.py @@ -1,8 +1,11 @@ +import graphene + from utils.graphene.enums import ( convert_enum_to_graphene_enum, get_enum_name_from_django_field, ) - +from lead.models import Lead +from quality_assurance.models import EntryReviewComment from .models import Notification NotificationTypeEnum = convert_enum_to_graphene_enum(Notification.Type, name='NotificationTypeEnum') @@ -15,3 +18,8 @@ (Notification.status, NotificationStatusEnum), ) } + + +class AssignmentContentTypeEnum(graphene.Enum): + LEAD = Lead._meta.model_name + ENTRY_REVIEW_COMMENT = EntryReviewComment._meta.model_name diff --git a/apps/notification/models.py b/apps/notification/models.py index 7f7acadce9..3f33daa423 100644 --- a/apps/notification/models.py +++ b/apps/notification/models.py @@ -2,6 +2,7 @@ from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils import timezone +from quality_assurance.models import EntryReviewComment from user.models import User from project.models import Project @@ -88,4 +89,6 @@ class Meta: @staticmethod def get_for(user): - return Assignment.objects.filter(created_for=user).distinct() + return Assignment.objects.filter( + created_for=user, + ).distinct() diff --git a/apps/notification/schema.py b/apps/notification/schema.py index 9145764fae..d367cdc3bb 100644 --- a/apps/notification/schema.py +++ b/apps/notification/schema.py @@ -15,6 +15,7 @@ from .enums import ( NotificationTypeEnum, NotificationStatusEnum, + AssignmentContentTypeEnum ) @@ -51,24 +52,22 @@ class AssignmentLeadDetailType(graphene.ObjectType): id = graphene.ID(required=True) title = graphene.String(required=True) - class Meta: - model = Lead - fields = ['id', 'title'] - -class AssignmentEntryDetailType(graphene.ObjectType): +class AssignmentProjectDetailType(graphene.ObjectType): id = graphene.ID(required=True) + title = graphene.String(required=True) -class AssignmentProjectDetailType(graphene.ObjectType): +class AssignmentEntryReviewCommentDetailType(graphene.ObjectType): id = graphene.ID(required=True) - title = graphene.String(required=True) + entry_id = graphene.ID(required=True) + lead_id = graphene.ID(required=True) class AssignmentContentDataType(graphene.ObjectType): - content_type = graphene.String(required=False) + content_type = graphene.Field(AssignmentContentTypeEnum) lead = graphene.Field(AssignmentLeadDetailType) - entry = graphene.Field(AssignmentEntryDetailType) + entry_review_comment = graphene.Field(AssignmentEntryReviewCommentDetailType) class AssignmentType(DjangoObjectType): From b6a961c575c89b3bae7dc93014c8f2dba942257d Mon Sep 17 00:00:00 2001 From: Sudan Bhandari Date: Mon, 29 Apr 2024 13:54:39 +0545 Subject: [PATCH 8/9] update testcase --- apps/entry/factories.py | 11 +- apps/notification/dataloaders.py | 6 +- apps/notification/models.py | 1 - apps/notification/schema.py | 5 +- apps/notification/tests/test_apis.py | 15 ++- apps/notification/tests/test_mutation.py | 89 ++++++++++++- apps/notification/tests/test_schemas.py | 152 +++++++++++++++++++---- deep/dataloaders.py | 1 + schema.graphql | 13 +- 9 files changed, 257 insertions(+), 36 deletions(-) diff --git a/apps/entry/factories.py b/apps/entry/factories.py index 580fb2bb61..6f49b618dc 100644 --- a/apps/entry/factories.py +++ b/apps/entry/factories.py @@ -4,7 +4,11 @@ from gallery.factories import FileFactory -from .models import Entry, Attribute +from .models import ( + Entry, + Attribute, + EntryComment, +) class EntryFactory(DjangoModelFactory): @@ -39,3 +43,8 @@ class EntryAttributeFactory(DjangoModelFactory): class Meta: model = Attribute + + +class EntryCommentFactory(DjangoModelFactory): + class Meta: + model = EntryComment diff --git a/apps/notification/dataloaders.py b/apps/notification/dataloaders.py index c16b64ad63..7e56e10ada 100644 --- a/apps/notification/dataloaders.py +++ b/apps/notification/dataloaders.py @@ -2,7 +2,6 @@ from promise import Promise -from entry.models import EntryComment from notification.models import Assignment from lead.models import Lead from quality_assurance.models import EntryReviewComment @@ -57,8 +56,9 @@ def batch_load_fn(self, keys): _result = { _id: { 'content_type': content_type, - 'lead': _lead_id_map.get(object_id) if content_type == 'lead' else None, - 'entry_review_comment': _entry_review_comment_id_map.get(object_id) if content_type == 'entryreviewcomment' else None, + 'lead': _lead_id_map.get(object_id) if content_type == Lead._meta.model else None, + 'entry_review_comment': + _entry_review_comment_id_map.get(object_id) if content_type == EntryReviewComment._meta.model else None, } for _id, content_type, object_id in assignment_qs } diff --git a/apps/notification/models.py b/apps/notification/models.py index 3f33daa423..9c9d81aec2 100644 --- a/apps/notification/models.py +++ b/apps/notification/models.py @@ -2,7 +2,6 @@ from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils import timezone -from quality_assurance.models import EntryReviewComment from user.models import User from project.models import Project diff --git a/apps/notification/schema.py b/apps/notification/schema.py index d367cdc3bb..961f9e0fc1 100644 --- a/apps/notification/schema.py +++ b/apps/notification/schema.py @@ -3,7 +3,6 @@ from django.db.models import QuerySet from graphene_django import DjangoObjectType from graphene_django_extras import DjangoObjectField, PageGraphqlPagination -from lead.models import Lead from utils.graphene.enums import EnumDescription from utils.graphene.types import CustomDjangoListObjectType @@ -111,3 +110,7 @@ class Query: @staticmethod def resolve_notifications(root, info, **kwargs) -> QuerySet: return get_user_notification_qs(info) + + @staticmethod + def resolve_assignments(root, info, **kwargs) -> QuerySet: + return Assignment.get_for(info.context.user) diff --git a/apps/notification/tests/test_apis.py b/apps/notification/tests/test_apis.py index d14b16196b..9c6d2498a2 100644 --- a/apps/notification/tests/test_apis.py +++ b/apps/notification/tests/test_apis.py @@ -1,5 +1,6 @@ import pytest from datetime import timedelta +from django.contrib.contenttypes.models import ContentType from deep.tests import TestCase from django.utils import timezone @@ -508,9 +509,17 @@ def test_assignment_is_done(self): project = self.create(Project) user1 = self.create(User) user2 = self.create(User) - assignment = self.create(Assignment, created_for=user1, project=project, created_by=user2) - self.create(Assignment, created_for=user1, project=project, created_by=user2) - self.create(Assignment, created_for=user1, project=project, created_by=user2) + lead = self.create(Lead, project=project) + kwargs = { + 'object_id': lead.id, + 'content_type': ContentType.objects.get_for_model(Lead), + 'project': project, + 'created_for': user1, + 'created_by': user2, + } + assignment = self.create(Assignment, **kwargs) + self.create(Assignment, **kwargs) + self.create(Assignment, **kwargs) url = '/api/v1/assignments/' self.authenticate(user1) diff --git a/apps/notification/tests/test_mutation.py b/apps/notification/tests/test_mutation.py index 2e61a9b42a..8cc5c66579 100644 --- a/apps/notification/tests/test_mutation.py +++ b/apps/notification/tests/test_mutation.py @@ -1,8 +1,12 @@ from utils.graphene.tests import GraphQLTestCase +from django.contrib.contenttypes.models import ContentType from user.factories import UserFactory -from notification.factories import NotificationFactory -from notification.models import Notification +from project.factories import ProjectFactory +from lead.factories import LeadFactory +from notification.factories import NotificationFactory, AssignmentFactory +from notification.models import Assignment, Notification +from lead.models import Lead class NotificationMutation(GraphQLTestCase): @@ -30,7 +34,6 @@ def _query_check(minput, **kwargs): minput=minput, **kwargs ) - minput = dict(id=notification.id, status=self.genum(Notification.Status.SEEN)) # -- Without login _query_check(minput, assert_for_error=True) @@ -48,3 +51,83 @@ def _query_check(minput, **kwargs): self.force_login(another_user) content = _query_check(minput, okay=False)['data']['notificationStatusUpdate']['result'] self.assertEqual(content, None, content) + + +class TestAssignmentMutation(GraphQLTestCase): + def test_assginment_bulk_status_mark_as_done(self): + self.assignment_query = ''' + mutation MyMutation { + assignmentBulkStatusMarkAsDone { + errors + ok + } + } + ''' + project = ProjectFactory.create() + user = UserFactory.create() + lead = LeadFactory.create() + AssignmentFactory.create_batch( + 3, + project=project, + object_id=lead.id, + content_type=ContentType.objects.get_for_model(Lead), + created_for=user, + is_done=False + ) + + def _query_check(**kwargs): + return self.query_check( + self.assignment_query, + **kwargs + ) + + self.force_login(user) + content = _query_check() + assignments_qs = Assignment.get_for(user).filter(is_done=False) + self.assertEqual(content['data']['assignmentBulkStatusMarkAsDone']['errors'], None) + self.assertEqual(len(assignments_qs), 0) + + def test_individual_assignment_update_status(self): + self.indivdual_assignment_query = ''' + mutation Mutation($isDone: Boolean, $id: ID! ){ + assignmentUpdate(id: $id, data: {isDone: $isDone}){ + ok + errors + result{ + id, + isDone + } + } + } + ''' + + user = UserFactory.create() + project = ProjectFactory.create() + lead = LeadFactory.create() + assignment = AssignmentFactory.create( + project=project, + object_id=lead.id, + content_type=ContentType.objects.get_for_model(Lead), + created_for=user, + is_done=False + + ) + + def _query_check(**kwargs): + return self.query_check( + self.indivdual_assignment_query, + variables={"isDone": True, "id": assignment.id}, + **kwargs + ) + + # without login + + _query_check(assert_for_error=True) + + # with normal login + + self.force_login(user) + content = _query_check() + assignment_qs = Assignment.get_for(user).filter(id=assignment.id, is_done=False) + self.assertEqual(content['data']['assignmentUpdate']['errors'], None) + self.assertEqual(len(assignment_qs), 0) diff --git a/apps/notification/tests/test_schemas.py b/apps/notification/tests/test_schemas.py index 29195196a3..afc2571313 100644 --- a/apps/notification/tests/test_schemas.py +++ b/apps/notification/tests/test_schemas.py @@ -1,13 +1,18 @@ import datetime import pytz +from django.contrib.contenttypes.models import ContentType from utils.graphene.tests import GraphQLTestCase +from lead.models import Lead +from notification.models import Assignment, Notification +from quality_assurance.models import EntryReviewComment + from user.factories import UserFactory from project.factories import ProjectFactory - -from notification.models import Notification from notification.factories import AssignmentFactory, NotificationFactory from lead.factories import LeadFactory +from entry.factories import EntryCommentFactory, EntryFactory +from analysis_framework.factories import AnalysisFrameworkFactory class TestNotificationQuerySchema(GraphQLTestCase): @@ -192,35 +197,140 @@ def _query_check(filters, **kwargs): class TestAssignmentQuerySchema(GraphQLTestCase): def test_assignments_query(self): query = ''' - query MyQuery { - assignments(isDone: false) { - results { - contentData { - contentType - entry { - id + query MyQuery { + assignments { + results { + contentData { + contentType + entryReviewComment { + entryId + id + leadId + } + lead { + id + title + } } - lead { + createdAt + id + isDone + objectId + project { id title } + createdFor { + id + } + createdBy { + id + } } - createdAt - id - isDone - objectId - project { - id - title - } + totalCount } } +''' + project = ProjectFactory.create() + user = UserFactory.create() + another = UserFactory.create() + lead = LeadFactory.create() + af = AnalysisFrameworkFactory.create() + entry = EntryFactory.create( + analysis_framework=af, + lead=lead + ) + entry_comment = EntryCommentFactory.create( + entry=entry, + created_by=user + ) + Assignment.objects.all().delete() + AssignmentFactory.create_batch( + 3, + project=project, + object_id=lead.id, + content_type=ContentType.objects.get_for_model(Lead), + created_for=user + ) + + def _query_check(**kwargs): + return self.query_check(query, **kwargs) + + # -- without login + _query_check(assert_for_error=True) + + # -- with login with different user + self.force_login(another) + content = _query_check() + self.assertEqual(content['data']['assignments']['results'], [], content) + # -- with login normal user + self.force_login(user) + content = _query_check() + self.assertEqual(content['data']['assignments']['totalCount'], 3) + self.assertEqual(content['data']['assignments']['results'][0]['contentData']['contentType'], 'LEAD') + AssignmentFactory.create_batch( + 3, + project=project, + object_id=entry_comment.id, + content_type=ContentType.objects.get_for_model(EntryReviewComment), + created_for=user + ) + content = _query_check() + self.assertEqual(content['data']['assignments']['totalCount'], 6) + + def test_assignments_with_filter_query(self): + query = ''' + query MyQuery ( + $isDone : Boolean, + ){ + assignments( + isDone: $isDone, + ){ + totalCount + results{ + id + } + } } ''' project = ProjectFactory.create() user = UserFactory.create() lead = LeadFactory.create() - assignment = AssignmentFactory.create( - project=project.id, - object_id=lead.id + af = AnalysisFrameworkFactory.create() + entry = EntryFactory.create( + analysis_framework=af, + lead=lead ) + entry_comment = EntryCommentFactory.create( + entry=entry, + created_by=user + ) + Assignment.objects.all().delete() + AssignmentFactory.create_batch( + 3, + project=project, + object_id=lead.id, + content_type=ContentType.objects.get_for_model(Lead), + created_for=user, + is_done=False + ) + AssignmentFactory.create_batch( + 5, + project=project, + object_id=entry_comment.id, + content_type=ContentType.objects.get_for_model(EntryReviewComment), + created_for=user, + is_done=True + ) + + def _query_check(filters, **kwargs): + return self.query_check(query, variables=filters, **kwargs) + + self.force_login(user) + for filters, count in [ + ({'isDone': True}, 5), + ({'isDone': False}, 3), + ]: + content = _query_check(filters) + self.assertEqual(content['data']['assignments']['totalCount'], count, f'\n{filters=} \n{content=}') + self.assertEqual(len(content['data']['assignments']['results']), count, f'\n{filters=} \n{content=}') diff --git a/deep/dataloaders.py b/deep/dataloaders.py index 61ca9f6890..05109008ce 100644 --- a/deep/dataloaders.py +++ b/deep/dataloaders.py @@ -76,5 +76,6 @@ def deep_gallery(self): def assisted_tagging(self): return AssistedTaggingLoaders(context=self.context) + @cached_property def notification(self): return AssignmentLoaders(context=self.context) diff --git a/schema.graphql b/schema.graphql index 259f6fd92c..273caa1913 100644 --- a/schema.graphql +++ b/schema.graphql @@ -3082,13 +3082,20 @@ type AssessmentType { } type AssignmentContentDataType { - contentType: String + contentType: AssignmentContentTypeEnum lead: AssignmentLeadDetailType - entry: AssignmentEntryDetailType + entryReviewComment: AssignmentEntryReviewCommentDetailType } -type AssignmentEntryDetailType { +enum AssignmentContentTypeEnum { + LEAD + ENTRY_REVIEW_COMMENT +} + +type AssignmentEntryReviewCommentDetailType { id: ID! + entryId: ID! + leadId: ID! } input AssignmentInputType { From 94ba125a1f4c56a6558535cdd9a503fb6adcaebb Mon Sep 17 00:00:00 2001 From: thenav56 Date: Thu, 2 May 2024 07:42:23 +0545 Subject: [PATCH 9/9] Fix content type cache issue in test --- apps/notification/dataloaders.py | 28 +++++++----- apps/notification/models.py | 5 +- apps/notification/schema.py | 1 + apps/notification/tests/test_apis.py | 6 ++- apps/notification/tests/test_mutation.py | 2 +- apps/notification/tests/test_schemas.py | 58 ++++++++++++------------ 6 files changed, 56 insertions(+), 44 deletions(-) diff --git a/apps/notification/dataloaders.py b/apps/notification/dataloaders.py index 7e56e10ada..464956c3ca 100644 --- a/apps/notification/dataloaders.py +++ b/apps/notification/dataloaders.py @@ -1,4 +1,5 @@ from django.utils.functional import cached_property +from django.db import models from promise import Promise @@ -9,6 +10,10 @@ from utils.graphene.dataloaders import DataLoaderWithContext, WithContextMixin +def get_model_name(model: models.Model) -> str: + return model._meta.model_name + + class AssignmentLoader(DataLoaderWithContext): def batch_load_fn(self, keys): assignment_qs = list( @@ -21,10 +26,9 @@ def batch_load_fn(self, keys): entry_review_comment_id = [] for _, content_type, object_id in assignment_qs: - # TODO use dict map function - if content_type == Lead._meta.model_name: + if content_type == get_model_name(Lead): leads_id.append(object_id) - elif content_type == EntryReviewComment._meta.model_name: + elif content_type == get_model_name(EntryReviewComment): entry_review_comment_id.append(object_id) _lead_id_map = {} @@ -37,31 +41,33 @@ def batch_load_fn(self, keys): _entry_review_comment_id_map = {} - for _id, text, entry_id, entry_excerpt, lead_id in EntryReviewComment.objects.filter( + for _id, entry_id, lead_id in EntryReviewComment.objects.filter( id__in=entry_review_comment_id).values_list( 'id', - 'comment_texts__text', 'entry__id', - 'entry__excerpt', 'entry__lead_id' ): _entry_review_comment_id_map[_id] = dict( id=_id, - text=text, entry_id=entry_id, - entry_excerpt=entry_excerpt, lead_id=lead_id ) _result = { _id: { 'content_type': content_type, - 'lead': _lead_id_map.get(object_id) if content_type == Lead._meta.model else None, - 'entry_review_comment': - _entry_review_comment_id_map.get(object_id) if content_type == EntryReviewComment._meta.model else None, + 'lead': ( + _lead_id_map.get(object_id) + if content_type == get_model_name(Lead) else None + ), + 'entry_review_comment': ( + _entry_review_comment_id_map.get(object_id) + if content_type == get_model_name(EntryReviewComment) else None + ), } for _id, content_type, object_id in assignment_qs } + return Promise.resolve([_result[key] for key in keys]) diff --git a/apps/notification/models.py b/apps/notification/models.py index 9c9d81aec2..a34d3d5917 100644 --- a/apps/notification/models.py +++ b/apps/notification/models.py @@ -88,6 +88,9 @@ class Meta: @staticmethod def get_for(user): + from entry.models import EntryComment return Assignment.objects.filter( created_for=user, - ).distinct() + ).exclude( + content_type__model=EntryComment._meta.model_name + ) # The EntryComment assignment are excluded need to remove later diff --git a/apps/notification/schema.py b/apps/notification/schema.py index 961f9e0fc1..2302bb9e14 100644 --- a/apps/notification/schema.py +++ b/apps/notification/schema.py @@ -76,6 +76,7 @@ class Meta: project = graphene.Field(AssignmentProjectDetailType) content_data = graphene.Field(AssignmentContentDataType) + @staticmethod def resolve_content_data(root, info): return info.context.dl.notification.assignment.load(root.pk) diff --git a/apps/notification/tests/test_apis.py b/apps/notification/tests/test_apis.py index 9c6d2498a2..0a5c9ddc2a 100644 --- a/apps/notification/tests/test_apis.py +++ b/apps/notification/tests/test_apis.py @@ -506,13 +506,15 @@ def test_assignment_create_on_entry_comment_assignee_change(self): assert data['count'] == 1 # assignment for user2 def test_assignment_is_done(self): + # XXX: To avoid using content type cache from pre-tests + ContentType.objects.clear_cache() + project = self.create(Project) user1 = self.create(User) user2 = self.create(User) lead = self.create(Lead, project=project) kwargs = { - 'object_id': lead.id, - 'content_type': ContentType.objects.get_for_model(Lead), + 'content_object': lead, 'project': project, 'created_for': user1, 'created_by': user2, diff --git a/apps/notification/tests/test_mutation.py b/apps/notification/tests/test_mutation.py index 8cc5c66579..bca376614f 100644 --- a/apps/notification/tests/test_mutation.py +++ b/apps/notification/tests/test_mutation.py @@ -94,7 +94,7 @@ def test_individual_assignment_update_status(self): ok errors result{ - id, + id isDone } } diff --git a/apps/notification/tests/test_schemas.py b/apps/notification/tests/test_schemas.py index afc2571313..eecf3b1bdb 100644 --- a/apps/notification/tests/test_schemas.py +++ b/apps/notification/tests/test_schemas.py @@ -2,16 +2,16 @@ import pytz from django.contrib.contenttypes.models import ContentType + from utils.graphene.tests import GraphQLTestCase -from lead.models import Lead -from notification.models import Assignment, Notification -from quality_assurance.models import EntryReviewComment +from notification.models import Notification from user.factories import UserFactory from project.factories import ProjectFactory from notification.factories import AssignmentFactory, NotificationFactory from lead.factories import LeadFactory -from entry.factories import EntryCommentFactory, EntryFactory +from entry.factories import EntryFactory +from quality_assurance.factories import EntryReviewCommentFactory from analysis_framework.factories import AnalysisFrameworkFactory @@ -230,7 +230,11 @@ def test_assignments_query(self): totalCount } } -''' + ''' + + # XXX: To avoid using content type cache from pre-tests + ContentType.objects.clear_cache() + project = ProjectFactory.create() user = UserFactory.create() another = UserFactory.create() @@ -240,17 +244,16 @@ def test_assignments_query(self): analysis_framework=af, lead=lead ) - entry_comment = EntryCommentFactory.create( + entry_comment = EntryReviewCommentFactory.create( entry=entry, created_by=user ) - Assignment.objects.all().delete() + AssignmentFactory.create_batch( 3, project=project, - object_id=lead.id, - content_type=ContentType.objects.get_for_model(Lead), - created_for=user + content_object=lead, + created_for=user, ) def _query_check(**kwargs): @@ -271,8 +274,7 @@ def _query_check(**kwargs): AssignmentFactory.create_batch( 3, project=project, - object_id=entry_comment.id, - content_type=ContentType.objects.get_for_model(EntryReviewComment), + content_object=entry_comment, created_for=user ) content = _query_check() @@ -280,19 +282,19 @@ def _query_check(**kwargs): def test_assignments_with_filter_query(self): query = ''' - query MyQuery ( - $isDone : Boolean, - ){ - assignments( - isDone: $isDone, - ){ - totalCount - results{ - id - } - } + query MyQuery($isDone: Boolean) { + assignments(isDone: $isDone) { + totalCount + results { + id + } + } } ''' + + # XXX: To avoid using content type cache from pre-tests + ContentType.objects.clear_cache() + project = ProjectFactory.create() user = UserFactory.create() lead = LeadFactory.create() @@ -301,24 +303,22 @@ def test_assignments_with_filter_query(self): analysis_framework=af, lead=lead ) - entry_comment = EntryCommentFactory.create( + entry_comment = EntryReviewCommentFactory.create( entry=entry, created_by=user ) - Assignment.objects.all().delete() + AssignmentFactory.create_batch( 3, project=project, - object_id=lead.id, - content_type=ContentType.objects.get_for_model(Lead), + content_object=lead, created_for=user, is_done=False ) AssignmentFactory.create_batch( 5, project=project, - object_id=entry_comment.id, - content_type=ContentType.objects.get_for_model(EntryReviewComment), + content_object=entry_comment, created_for=user, is_done=True )