diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 006fa92c1b..9f258522b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: env: DOCKER_IMAGE_SERVER: ${{ steps.prep.outputs.tagged_image }} run: | - docker-compose -f ./gh-docker-compose.yml run --rm server bash -c 'wait-for-it db:5432 && ./manage.py graphql_schema --out /ci-share/schema-latest.graphql' && + docker compose -f ./gh-docker-compose.yml run --rm server bash -c 'wait-for-it db:5432 && ./manage.py graphql_schema --out /ci-share/schema-latest.graphql' && cmp --silent schema.graphql ./ci-share/schema-latest.graphql || { echo 'The schema.graphql is not up to date with the latest changes. Please update and push latest'; diff schema.graphql ./ci-share/schema-latest.graphql; @@ -53,7 +53,7 @@ jobs: env: DOCKER_IMAGE_SERVER: ${{ steps.prep.outputs.tagged_image }} run: | - docker-compose -f ./gh-docker-compose.yml run --rm server bash -c 'wait-for-it db:5432 && ./manage.py makemigrations --check --dry-run' || { + docker compose -f ./gh-docker-compose.yml run --rm server bash -c 'wait-for-it db:5432 && ./manage.py makemigrations --check --dry-run' || { echo 'There are some changes to be reflected in the migration. Make sure to run makemigrations'; exit 1; } @@ -64,7 +64,7 @@ jobs: CC_TEST_REPORTER_ID: ${{ secrets.CODE_CLIMATE_ID }} DOCKER_IMAGE_SERVER: ${{ steps.prep.outputs.tagged_image }} with: - coverageCommand: docker-compose -f gh-docker-compose.yml run --rm server /code/scripts/run_tests.sh + coverageCommand: docker compose -f gh-docker-compose.yml run --rm server /code/scripts/run_tests.sh coverageLocations: | ${{github.workspace}}/coverage/coverage.xml:coverage.py diff --git a/.travis-old.yml b/.travis-old.yml index 188c3073ae..389eb151f7 100644 --- a/.travis-old.yml +++ b/.travis-old.yml @@ -14,7 +14,7 @@ before_install: - env > .env - mv travis-docker-compose.yml docker-compose.yml - - docker-compose pull && docker pull ${DOCKER_IMAGE_SERVER_B} || true + - docker compose pull && docker pull ${DOCKER_IMAGE_SERVER_B} || true - | docker build\ --cache-from $(get_image ${DOCKER_IMAGE_SERVER_B} ${DOCKER_IMAGE_SERVER})\ @@ -24,12 +24,12 @@ before_script: - /tmp/cc-test-reporter before-build script: - - docker-compose run server /code/scripts/run_tests.sh + - docker compose run server /code/scripts/run_tests.sh after_success: - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - /tmp/cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT - - docker-compose push server + - docker compose push server - docker push ${DOCKER_IMAGE_SERVER_B} deploy: diff --git a/README.md b/README.md index 78c23836c0..2ff399cc57 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ Add this to your `.git/hooks/pre-commit` to generate latest graphql schema befor echo "pre-commit: Generating graphql schema." if [ -z `docker ps -q --no-trunc | grep $(docker-compose ps -q web)` ]; then - docker-compose run --rm web ./manage.py graphql_schema --out schema.graphql + docker compose run --rm web ./manage.py graphql_schema --out schema.graphql else - docker-compose exec -T web ./manage.py graphql_schema --out schema.graphql + docker compose exec -T web ./manage.py graphql_schema --out schema.graphql fi ``` FYI: If hooks aren't working https://stackoverflow.com/questions/49912695/git-pre-and-post-commit-hooks-not-running diff --git a/apps/analysis_framework/models.py b/apps/analysis_framework/models.py index 88a583d120..30908ec616 100644 --- a/apps/analysis_framework/models.py +++ b/apps/analysis_framework/models.py @@ -1,5 +1,5 @@ import copy -from typing import Union +from typing import Union, Optional from django.db import models from django.core.exceptions import ValidationError @@ -63,15 +63,13 @@ def __init__(self, *args, **kwargs): def __str__(self): return self.title - def clone(self, user, overrides={}): + def clone(self, user, title: Optional[str] = None, description: Optional[str] = None): """ Clone analysis framework along with all widgets, filters and exportables """ - title = overrides.get( - 'title', '{} (cloned)'.format(self.title[:230]) - ) # Strip off extra chars from title - description = overrides.get('description', '') + title = title or "{} (cloned)".format(self.title[:230]) # Strip off extra chars from title + description = description or "" clone_analysis_framework = AnalysisFramework( title=title, description=description, diff --git a/apps/analysis_framework/mutation.py b/apps/analysis_framework/mutation.py index 2ff0a81598..fda86480bd 100644 --- a/apps/analysis_framework/mutation.py +++ b/apps/analysis_framework/mutation.py @@ -102,13 +102,16 @@ def get_valid_delete_items(cls, info, delete_ids): ) -class CloneAnalysisFramework(AfGrapheneMutation): +class CloneAnalysisFramework(GrapheneMutation): class Arguments: data = AnalysisFrameworkCloneInputType(required=True) result = graphene.Field(AnalysisFrameworkDetailType) serializer_class = AnalysisFrameworkCloneSerializer - permissions = [AfP.Permission.CAN_CLONE_FRAMEWORK] + + @classmethod + def check_permissions(cls, info, **_): + return True # NOTE: Checking permissions in serializer class AnalysisFrameworkMutationType(DjangoObjectType): @@ -117,7 +120,6 @@ class AnalysisFrameworkMutationType(DjangoObjectType): """ analysis_framework_update = UpdateAnalysisFramework.Field() analysis_framework_membership_bulk = BulkUpdateAnalysisFrameworkMembership.Field() - analysis_framework_clone = CloneAnalysisFramework.Field() class Meta: model = AnalysisFramework @@ -136,4 +138,5 @@ def get_custom_node(_, info, id): class Mutation(object): analysis_framework_create = CreateAnalysisFramework.Field() + analysis_framework_clone = CloneAnalysisFramework.Field() analysis_framework = DjangoObjectField(AnalysisFrameworkMutationType) diff --git a/apps/analysis_framework/serializers.py b/apps/analysis_framework/serializers.py index c840ec7cad..1605a637ab 100644 --- a/apps/analysis_framework/serializers.py +++ b/apps/analysis_framework/serializers.py @@ -712,18 +712,39 @@ def create(self, validated_data): class AnalysisFrameworkCloneSerializer(serializers.Serializer): + af_id = IntegerIDField(required=True) title = serializers.CharField(required=True) description = serializers.CharField(required=False) project = serializers.PrimaryKeyRelatedField(queryset=Project.objects.all(), required=False) + def validate_af_id(self, af_id): + af = ( + AnalysisFramework + .get_for_gq(self.context['request'].user, only_member=False) + .filter(pk=af_id) + .first() + ) + if af is None: + raise serializers.ValidationError('Analysis Framework does not exist') + if not af.can_clone(self.context['request'].user): + raise serializers.ValidationError( + 'User does not have permission to modify the AF' + ) + return af + def validate_project(self, project): if not project.can_modify(self.context['request'].user): raise serializers.ValidationError('User does not have permission to modify the project') return project def create(self, validated_data): - af = self.context['request'].active_af - new_af = af.clone(self.context['request'].user, validated_data) + # NOTE: af is an obj, check validate_af_id + af = validated_data['af_id'] + new_af = af.clone( + self.context['request'].user, + title=validated_data['title'], + description=validated_data.get('description') + ) new_af.add_member(self.context['request'].user, new_af.get_or_create_owner_role()) if project := validated_data.get('project'): diff --git a/apps/analysis_framework/tests/test_mutations.py b/apps/analysis_framework/tests/test_mutations.py index 9b616a47a2..2e67357b91 100644 --- a/apps/analysis_framework/tests/test_mutations.py +++ b/apps/analysis_framework/tests/test_mutations.py @@ -786,18 +786,16 @@ def _query_check(**kwargs): def test_analysis_framework_clone(self): query = ''' - mutation MyMutation ($id: ID!, $input: AnalysisFrameworkCloneInputType!) { + mutation MyMutation ($input: AnalysisFrameworkCloneInputType!) { __typename - analysisFramework (id: $id ) { - analysisFrameworkClone(data: $input) { - ok - errors - result { - id - title - description - clonedFrom - } + analysisFrameworkClone(data: $input) { + ok + errors + result { + id + title + description + clonedFrom } } } @@ -815,65 +813,60 @@ def test_analysis_framework_clone(self): private_af = AnalysisFrameworkFactory.create(created_by=member_user, title='AF Private Orginal', is_private=True) private_af.add_member(low_permission_user) - minput = dict( + public_minput = dict( + title='AF (TEST)', + description='Af description', + afId=af.id, + ) + private_minput = dict( title='AF (TEST)', description='Af description', + afId=private_af.id, ) def _public_af_query_check(**kwargs): return self.query_check( query, - minput=minput, - mnested=['analysisFramework'], - variables={'id': af.id}, + minput=public_minput, **kwargs, ) def _private_af_query_check(**kwargs): return self.query_check( query, - minput=minput, - mnested=['analysisFramework'], - variables={'id': private_af.id}, + minput=private_minput, **kwargs, ) # ---------- Without login _public_af_query_check(assert_for_error=True) - # ---------- With login - self.force_login(non_member_user) - # ---------- Let's Clone a new AF (Using create test data) - _public_af_query_check(assert_for_error=True) - # ---------- With login (with access member) - self.force_login(member_user) + # ---------- With login (with non access member) on public AF + self.force_login(non_member_user) response = _public_af_query_check() - self.assertEqual(response['data']['analysisFramework']['analysisFrameworkClone']['result']['clonedFrom'], str(af.id)) + self.assertEqual(response['data']['analysisFrameworkClone']['result']['clonedFrom'], str(af.id)) - # adding project to the input - minput['project'] = project.id + # adding project to the inputs + public_minput['project'] = project.id + private_minput['project'] = project.id # with Login (non project member) self.force_login(non_member_user) - _public_af_query_check(assert_for_error=True) - - # with Login (project member with no permission on AF) - self.force_login(low_permission_user) - _public_af_query_check(assert_for_error=True) + _public_af_query_check(okay=False) # with Login (project member with no permission on Private AF) self.force_login(member_user) - _private_af_query_check(assert_for_error=True) + _private_af_query_check(okay=False) # With Login (project member with permission on Public AF) self.force_login(member_user) - response = _public_af_query_check()['data']['analysisFramework'] + response = _public_af_query_check()['data'] project.refresh_from_db() self.assertEqual(str(project.analysis_framework_id), response['analysisFrameworkClone']['result']['id']) # with Login (project member with permission on Private AF) private_af.add_member(member_user, role=self.af_owner) - response = _private_af_query_check()['data']['analysisFramework'] + response = _private_af_query_check()['data'] project.refresh_from_db() self.assertEqual(str(project.analysis_framework_id), response['analysisFrameworkClone']['result']['id']) diff --git a/apps/analysis_framework/views.py b/apps/analysis_framework/views.py index 3caf3f4418..080ee1113a 100644 --- a/apps/analysis_framework/views.py +++ b/apps/analysis_framework/views.py @@ -108,7 +108,8 @@ def post(self, request, af_id, version=None): new_af = analysis_framework.clone( request.user, - request.data or {}, + title=cloned_title, + description=request.data.get('description'), ) # Set the requesting user as owner member, don't create other memberships of old framework new_af.add_member(request.user, new_af.get_or_create_owner_role()) diff --git a/schema.graphql b/schema.graphql index 51764b5fe8..946f2e0df5 100644 --- a/schema.graphql +++ b/schema.graphql @@ -58,6 +58,7 @@ type AnalysisAutomaticSummaryType { } input AnalysisFrameworkCloneInputType { + afId: ID! title: String! description: String project: ID @@ -148,7 +149,6 @@ type AnalysisFrameworkMutationType { title: String! analysisFrameworkUpdate(data: AnalysisFrameworkInputType!): UpdateAnalysisFramework analysisFrameworkMembershipBulk(deleteIds: [ID!], items: [BulkAnalysisFrameworkMembershipInputType!]): BulkUpdateAnalysisFrameworkMembership - analysisFrameworkClone(data: AnalysisFrameworkCloneInputType!): CloneAnalysisFramework } enum AnalysisFrameworkPermission { @@ -5066,6 +5066,7 @@ type Mutation { updateMe(data: UserMeInputType!): UpdateMe deleteUser: UserDelete analysisFrameworkCreate(data: AnalysisFrameworkInputType!): CreateAnalysisFramework + analysisFrameworkClone(data: AnalysisFrameworkCloneInputType!): CloneAnalysisFramework analysisFramework(id: ID!): AnalysisFrameworkMutationType }