diff --git a/apps/analysis/mutation.py b/apps/analysis/mutation.py index c3bdcc0b61..66da151fe8 100644 --- a/apps/analysis/mutation.py +++ b/apps/analysis/mutation.py @@ -35,6 +35,7 @@ AnalysisType, ) from .serializers import ( + AnalysisCloneGqlSerializer, AnalysisPillarGqlSerializer, DiscardedEntryGqlSerializer, AnalysisTopicModelSerializer, @@ -113,6 +114,11 @@ serializer_class=AnalysisGqlSerializer, ) +AnalysisCloneInputType = generate_input_type_for_serializer( + 'AnalysisCloneInputType', + serializer_class=AnalysisCloneGqlSerializer +) + class RequiredPermissionMixin(): permissions = [ @@ -314,6 +320,14 @@ class Arguments: result = graphene.Field(AnalysisPillarType) +class AnalysisClone(AnalysisMutationMixin, PsGrapheneMutation): + class Arguments: + data = AnalysisCloneInputType(required=True) + model = Analysis + serializer_class = AnalysisCloneGqlSerializer + result = graphene.Field(AnalysisType) + + class Mutation(): # Analysis Pillar analysis_pillar_update = UpdateAnalysisPillar.Field() @@ -339,3 +353,6 @@ class Mutation(): analysis_create = CreateAnalysis.Field() analysis_update = UpdateAnalysis.Field() analysis_delete = DeleteAnalysis.Field() + + # AnalysisClone + analysis_clone = AnalysisClone.Field() diff --git a/apps/analysis/serializers.py b/apps/analysis/serializers.py index 67fe857bac..ec705ce8ae 100644 --- a/apps/analysis/serializers.py +++ b/apps/analysis/serializers.py @@ -479,7 +479,37 @@ def update(self, instance, validated_data): return super().update(instance, validated_data) -AnalysisCloneGqlSerializer = AnalysisCloneInputSerializer +class AnalysisCloneGqlSerializer(serializers.Serializer): + analysis_id = IntegerIDField() + title = serializers.CharField(required=True, write_only=True) + start_date = serializers.DateField(write_only=True, required=False, allow_null=True) + end_date = serializers.DateField(required=True, write_only=True) + + def validate(self, data): + start_date = data.get('start_date') + end_date = data.get('end_date') + if start_date and start_date > end_date: + raise serializers.ValidationError( + {'end_date': 'End date must occur after start date'} + ) + return data + + def validate_analysis_id(self, analysis_id): + analysis = Analysis.objects.filter( + project=self.context['request'].active_project, + pk=analysis_id + ).first() + if analysis is None: + raise serializers.ValidationError("Analysis does not exists") + return analysis + + def create(self, validated_data): + title = validated_data['title'] + end_date = validated_data['end_date'] + # NOTE validated_data['analysis_id'] is an object of analysis + analysis = validated_data['analysis_id'] + analysis.clone_analysis(title, end_date) + return analysis class AnalysisTopicModelSerializer(UserResourceSerializer, serializers.ModelSerializer): diff --git a/apps/analysis/tests/test_mutations.py b/apps/analysis/tests/test_mutations.py index 3b4f5e675f..d0c8b380b9 100644 --- a/apps/analysis/tests/test_mutations.py +++ b/apps/analysis/tests/test_mutations.py @@ -19,9 +19,12 @@ AnalysisPillarFactory, AnalysisReportFactory, AnalysisReportUploadFactory, + AnalyticalStatementFactory, + DiscardedEntryFactory, ) from analysis.models import ( + DiscardedEntry, TopicModel, TopicModelCluster, AutomaticSummary, @@ -1732,3 +1735,84 @@ def _query_check(**kwargs): self.assertEqual(analysis_pillar_resp_data['title'], self.update_minput['analysisPillarUpdate']['title']) self.assertEqual(analysis_pillar_resp_data['id'], str(self.update_minput['analysisPillarID'])) self.assertEqual(analysis_pillar_resp_data['analysisId'], str(self.analysis.id)) + + +class TestCloneAnalysisMutationSchema(GraphQLTestCase): + ANALYSIS_CLONE_MUTATION = ''' + mutation AnalysisClone($projectId: ID!, $data: AnalysisCloneInputType!) { + project(id: $projectId) { + analysisClone(data: $data) { + ok + errors + result { + id + title + endDate + } + __typename + } + __typename + } + } + ''' + + def setUp(self): + super().setUp() + self.project = ProjectFactory.create() + self.member_user = UserFactory.create() + self.non_member_user = UserFactory.create() + self.readonly_member_user = UserFactory.create() + self.project.add_member(self.readonly_member_user, role=self.project_role_reader_non_confidential) + af = AnalysisFrameworkFactory.create() + project = ProjectFactory.create(analysis_framework=af) + lead = LeadFactory.create(project=project) + self.project.add_member(self.member_user, role=self.project_role_member) + entry = EntryFactory.create(project=project, lead=lead) + EntryFactory.create(project=project, lead=lead) + self.analysis = AnalysisFactory.create( + project=project, + team_lead=self.member_user, + end_date=datetime.date(2022, 4, 1), + + ) + pillar = AnalysisPillarFactory.create(analysis=self.analysis, title='title1', assignee=self.member_user) + AnalyticalStatementFactory.create( + analysis_pillar=pillar, + statement='Hello from here', + client_id='1', + ) + DiscardedEntryFactory.create( + entry=entry, + analysis_pillar=pillar, + tag=DiscardedEntry.TagType.REDUNDANT + ) + + def test_clone_analysis(self): + def _query_check(**kwargs): + return self.query_check( + self.ANALYSIS_CLONE_MUTATION, + variables=self.minput, + **kwargs + ) + self.minput = dict( + data=dict( + analysisId=self.analysis.id, + title='cloned_title', + endDate="2022-04-01", + ), + projectId=self.project.id, + ) + # without login + _query_check(assert_for_error=True) + + # With login (non-member) + self.force_login(self.non_member_user) + _query_check(assert_for_error=True) + + # member user (read-only) + self.force_login(self.readonly_member_user) + _query_check(assert_for_error=True) + + # member user + self.force_login(self.member_user) + _query_check(assert_for_error=False) diff --git a/schema.graphql b/schema.graphql index 2c1519b1f2..5281090510 100644 --- a/schema.graphql +++ b/schema.graphql @@ -71,6 +71,19 @@ type AnalysisAutomaticSummaryType { status: AutomaticSummaryStatusEnum! } +type AnalysisClone { + errors: [GenericScalar!] + ok: Boolean + result: AnalysisType +} + +input AnalysisCloneInputType { + analysisId: ID! + title: String! + startDate: Date + endDate: Date! +} + input AnalysisFrameworkCloneInputType { afId: ID! title: String! @@ -5521,6 +5534,7 @@ type ProjectMutationType { analysisCreate(data: AnalysisInputType!): CreateAnalysis analysisUpdate(data: AnalysisInputType!, id: ID!): UpdateAnalysis analysisDelete(id: ID!): DeleteAnalysis + analysisClone(data: AnalysisCloneInputType!): AnalysisClone exportCreate(data: ExportCreateInputType!): CreateUserExport exportUpdate(data: ExportUpdateInputType!, id: ID!): UpdateUserExport exportCancel(id: ID!): CancelUserExport