Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add release note mutations #248

Merged
merged 3 commits into from
Nov 18, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 74 additions & 3 deletions coordinator/graphql/release_notes.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
from django_filters import CharFilter, FilterSet, NumberFilter, OrderingFilter
from graphene import relay, ObjectType
import graphene
from graphene_django.types import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField
from graphql import GraphQLError
from graphql_relay import from_global_id

from coordinator.api.models.release_note import ReleaseNote
from coordinator.api.models.release import Release
from coordinator.api.models.study import Study
from .releases import ReleaseNode
from .studies import StudyNode


class ReleaseNoteNode(DjangoObjectType):
Expand All @@ -12,7 +18,7 @@ class ReleaseNoteNode(DjangoObjectType):
class Meta:
model = ReleaseNote
filter_fields = {}
interfaces = (relay.Node,)
interfaces = (graphene.relay.Node,)


class ReleaseNoteFilter(FilterSet):
Expand All @@ -25,8 +31,67 @@ class Meta:
fields = ["kf_id", "author", "release", "study"]


class ReleaseNoteInput(graphene.InputObjectType):
description = graphene.String(
required=True,
description="Description of changes made to a study within a release",
)
study = graphene.ID(
required=True, description="Study that the note describes"
)
release = graphene.ID(
required=True,
description="Release that the study the note describes is in",
)


class CreateReleaseNote(graphene.Mutation):
class Arguments:
input = ReleaseNoteInput(required=True)

release_note = graphene.Field(ReleaseNoteNode)

@staticmethod
def mutate(root, info, input=None):
"""
Create a new release note for a study in a release.
"""
user = info.context.user
if not hasattr(user, "auth_roles") or (
"ADMIN" not in user.auth_roles and "DEV" not in user.auth_roles
):
raise GraphQLError("Not authenticated to create a release note.")

try:
_, study_id = from_global_id(input.get("study"))
study = Study.objects.get(kf_id=study_id)
del input["study"]
except Study.DoesNotExist as err:
raise GraphQLError(f"Study {study_id} does not exist")

try:
_, release_id = from_global_id(input.get("release"))
release = Release.objects.get(kf_id=release_id)
del input["release"]
except Release.DoesNotExist as err:
raise GraphQLError(f"Release {release_id} does not exist")

if not release.studies.filter(pk=study_id).exists():
raise GraphQLError(
f"Study {study_id} is not in release {release_id}"
)

release_note = ReleaseNote(**input)
release_note.author = user.username
release_note.release = release
release_note.study = study
release_note.save()

return CreateReleaseNote(release_note=release_note)


class Query:
event = relay.Node.Field(
event = graphene.relay.Node.Field(
ReleaseNoteNode, description="Retrieve a single release note"
)
all_release_notes = DjangoFilterConnectionField(
Expand Down Expand Up @@ -55,3 +120,9 @@ def resolve_all_release_notes(self, info, **kwargs):
) | ReleaseNote.objects.filter(release__state="published")

return ReleaseNote.objects.filter(release__state="published").all()


class Mutation:
create_release_note = CreateReleaseNote.Field(
description="Create a new release note for a given study in release"
)
11 changes: 9 additions & 2 deletions coordinator/graphql/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
Mutation as TaskServiceMutation,
)
from .events import Query as EventQuery
from .release_notes import Query as ReleaseNoteQuery
from .release_notes import (
Query as ReleaseNoteQuery,
Mutation as ReleaseNoteMutation,
)
from .studies import Query as StudyQuery, Mutation as StudyMutation
from .users import Query as UserQuery

Expand Down Expand Up @@ -50,7 +53,11 @@ def resolve_status(parent, info):


class Mutation(
ObjectType, ReleaseMutation, TaskServiceMutation, StudyMutation
ObjectType,
ReleaseMutation,
TaskServiceMutation,
StudyMutation,
ReleaseNoteMutation,
):
pass

Expand Down
161 changes: 161 additions & 0 deletions tests/graphql/release_notes/test_create_release_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import pytest
from graphql_relay.node.node import to_global_id
from coordinator.api.models import ReleaseNote
from coordinator.api.factories.study import StudyFactory
from coordinator.api.factories.release import ReleaseFactory


CREATE_RELEASE_NOTE = """
mutation ($input: ReleaseNoteInput!) {
createReleaseNote(input: $input) {
releaseNote {
id
kfId
uuid
author
description
createdAt
study {
id
kfId
}
release {
id
kfId
}
}
}
}
"""


@pytest.mark.parametrize(
"user_type,expected",
[("admin", True), ("dev", True), ("user", False), ("anon", False)],
)
def test_create_release_note_permissions(db, test_client, user_type, expected):
"""
ADMIN - Can create new release notes
DEV - Can create new release notes
USER - May not create relase notes
anonomous - May not create release notes
"""
study = StudyFactory()
study_id = to_global_id("StudyNode", study.kf_id)
release = ReleaseFactory(studies=[study])
release_id = to_global_id("ReleaseNode", release.kf_id)

variables = {
"input": {
"description": "Test note",
"study": study_id,
"release": release_id,
}
}

client = test_client(user_type)
resp = client.post(
"/graphql",
format="json",
data={"query": CREATE_RELEASE_NOTE, "variables": variables},
)

if expected:
assert (
"kfId" in resp.json()["data"]["createReleaseNote"]["releaseNote"]
)
else:
assert "errors" in resp.json()


def test_create_release_note(db, admin_client):
"""
Test that releases are created correctly.
"""
study = StudyFactory()
study_id = to_global_id("StudyNode", study.kf_id)
release = ReleaseFactory(studies=[study])
release_id = to_global_id("ReleaseNode", release.kf_id)

variables = {
"input": {
"description": "Test note",
"study": study_id,
"release": release_id,
}
}

resp = admin_client.post(
"/graphql",
format="json",
data={"query": CREATE_RELEASE_NOTE, "variables": variables},
)

release = resp.json()["data"]["createReleaseNote"]["releaseNote"]
assert ReleaseNote.objects.count() == 1
assert release["kfId"] == ReleaseNote.objects.first().kf_id
assert release["author"] == "bobby"


@pytest.mark.parametrize("entity", ["study", "release"])
def test_create_release_note_not_exist(db, admin_client, entity):
"""
Test that release notes may only be made for entities that exist
"""
study = StudyFactory()
study_id = to_global_id("StudyNode", study.kf_id)
release = ReleaseFactory(studies=[study])
release_id = to_global_id("ReleaseNode", release.kf_id)

if entity == "study":
study.delete()
if entity == "release":
release.delete()

variables = {
"input": {
"description": "Test note",
"study": study_id,
"release": release_id,
}
}

resp = admin_client.post(
"/graphql",
format="json",
data={"query": CREATE_RELEASE_NOTE, "variables": variables},
)

assert "errors" in resp.json()
error = resp.json()["errors"]
assert "does not exist" in error[0]["message"]


def test_create_release_note_study_not_in_release(db, admin_client):
"""
Test that notes may only be created for releases that contain the desired
study
"""
study = StudyFactory()
study_id = to_global_id("StudyNode", study.kf_id)
# The release will not contain the above study
release = ReleaseFactory()
release_id = to_global_id("ReleaseNode", release.kf_id)

variables = {
"input": {
"description": "Test note",
"study": study_id,
"release": release_id,
}
}

resp = admin_client.post(
"/graphql",
format="json",
data={"query": CREATE_RELEASE_NOTE, "variables": variables},
)

assert "errors" in resp.json()
error = resp.json()["errors"]
assert "is not in release" in error[0]["message"]