Skip to content

Take kausal_common mutations for registering/deleting users into use #119

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion kausal_common
27 changes: 10 additions & 17 deletions paths/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from nodes.schema import Mutations as NodesMutations, Query as NodesQuery
from pages.schema import Query as PagesQuery
from params.schema import Mutations as ParamsMutations, Query as ParamsQuery, types as params_types
from users.schema import Mutations as UsersMutations, Query as UsersQuery
from users.schema import Query as UsersQuery

if TYPE_CHECKING:
from kausal_common.graphene import GQLInfo
Expand Down Expand Up @@ -86,7 +86,7 @@ def resolve_unit(root: Query, info: GQLInfo, value: str) -> Unit:
return unit


class Mutations(ParamsMutations, NodesMutations, FrameworksMutations, UsersMutations):
class Mutations(ParamsMutations, NodesMutations, FrameworksMutations):
pass


Expand Down Expand Up @@ -114,36 +114,29 @@ class SBNode:


@sb.type(name='Query')
class SBQuery:
class SBQuery: # FIXME this does not seem to have any effect at the moment
@sb.field
def node(self, info: SBInfo, id: str) -> SBNode:
context = info.context.instance.context
node = context.get_node(id)
return SBNode(id=cast(sb.ID, node.id))


SB_MUTATION_TYPES: list[type] = []
if test_mode_enabled():
SB_MUTATION_TYPES.append(TestModeMutations)

SBMutation: type | None = None
if SB_MUTATION_TYPES:
SBMutation = merge_types('Mutation', tuple(SB_MUTATION_TYPES))


def generate_strawberry_schema() -> sb.Schema:
def generate_strawberry_schema(query: type, mutation: type | None = None) -> sb.Schema:
from kausal_common.strawberry.registry import strawberry_types

sb_schema = sb.Schema(
query=SBQuery, mutation=SBMutation, types=strawberry_types, directives=[context_directive]
# TODO: Add DjangoOptimizerExtension?
# https://strawberry.rocks/docs/django/guide/optimizer
query=query, mutation=mutation, types=strawberry_types, directives=[context_directive]
)
return sb_schema


def generate_schema() -> tuple[sb.Schema, CombinedSchema]:
def generate_schema(sb_query: type, sb_mutation: type | None = None) -> tuple[sb.Schema, CombinedSchema]:
# We generate the Strawberry schema just to be able to utilize the
# resolved GraphQL types directly in the Graphene schema.
sb_schema = generate_strawberry_schema()
sb_schema = generate_strawberry_schema(sb_query, sb_mutation)

schema = CombinedSchema(
sb_schema=sb_schema,
Expand All @@ -155,4 +148,4 @@ def generate_schema() -> tuple[sb.Schema, CombinedSchema]:
return sb_schema, schema


sb_schema, schema = generate_schema()
sb_schema, schema = generate_schema(SBQuery)
18 changes: 18 additions & 0 deletions paths/schema_test_mode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from __future__ import annotations

Check warning on line 1 in paths/schema_test_mode.py

View check run for this annotation

Kausal Code Coverage / codecov/patch

paths/schema_test_mode.py#L1

Added line #L1 was not covered by tests

from strawberry.tools import merge_types

Check warning on line 3 in paths/schema_test_mode.py

View check run for this annotation

Kausal Code Coverage / codecov/patch

paths/schema_test_mode.py#L3

Added line #L3 was not covered by tests

from kausal_common.deployment import test_mode_enabled
from kausal_common.testing.schema import TestModeMutations

Check warning on line 6 in paths/schema_test_mode.py

View check run for this annotation

Kausal Code Coverage / codecov/patch

paths/schema_test_mode.py#L5-L6

Added lines #L5 - L6 were not covered by tests

from paths.schema import SBQuery, generate_schema

Check warning on line 8 in paths/schema_test_mode.py

View check run for this annotation

Kausal Code Coverage / codecov/patch

paths/schema_test_mode.py#L8

Added line #L8 was not covered by tests

SB_MUTATION_TYPES: list[type] = []

Check warning on line 10 in paths/schema_test_mode.py

View check run for this annotation

Kausal Code Coverage / codecov/patch

paths/schema_test_mode.py#L10

Added line #L10 was not covered by tests
if test_mode_enabled():
SB_MUTATION_TYPES.append(TestModeMutations)

Check warning on line 12 in paths/schema_test_mode.py

View check run for this annotation

Kausal Code Coverage / codecov/patch

paths/schema_test_mode.py#L12

Added line #L12 was not covered by tests

SBMutation: type | None = None

Check warning on line 14 in paths/schema_test_mode.py

View check run for this annotation

Kausal Code Coverage / codecov/patch

paths/schema_test_mode.py#L14

Added line #L14 was not covered by tests
if SB_MUTATION_TYPES:
SBMutation = merge_types('Mutation', tuple(SB_MUTATION_TYPES))

Check warning on line 16 in paths/schema_test_mode.py

View check run for this annotation

Kausal Code Coverage / codecov/patch

paths/schema_test_mode.py#L16

Added line #L16 was not covered by tests

sb_schema, schema = generate_schema(SBQuery, SBMutation)

Check warning on line 18 in paths/schema_test_mode.py

View check run for this annotation

Kausal Code Coverage / codecov/patch

paths/schema_test_mode.py#L18

Added line #L18 was not covered by tests
8 changes: 6 additions & 2 deletions paths/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from corsheaders.defaults import default_headers as default_cors_headers

from kausal_common import ENV_SCHEMA as COMMON_ENV_SCHEMA, register_settings as register_common_settings
from kausal_common.deployment import set_secret_file_vars
from kausal_common.deployment import set_secret_file_vars, test_mode_enabled
from kausal_common.deployment.http import get_allowed_cors_headers
from kausal_common.sentry.init import init_sentry

Expand Down Expand Up @@ -290,8 +290,12 @@
SESSION_COOKIE_SAMESITE = 'None'
SESSION_COOKIE_SECURE = True

if test_mode_enabled():
GRAPHENE_SCHEMA = f'{PROJECT_NAME}.schema_test_mode.schema'
else:
GRAPHENE_SCHEMA = f'{PROJECT_NAME}.schema.schema'
GRAPHENE = {
'SCHEMA': f'{PROJECT_NAME}.schema.schema',
'SCHEMA': GRAPHENE_SCHEMA,
}
GRAPPLE = {
'APPS': ['pages'],
Expand Down
78 changes: 32 additions & 46 deletions users/schema.py
Original file line number Diff line number Diff line change
@@ -1,66 +1,52 @@
from __future__ import annotations

from typing import TYPE_CHECKING
import dataclasses

import graphene
from graphene_django import DjangoObjectType
from graphql import GraphQLError
import strawberry
from strawberry.types.field import StrawberryField

from .models import User
from kausal_common.strawberry.registry import register_strawberry_type

if TYPE_CHECKING:
from collections.abc import Sequence
from frameworks.roles import FrameworkRoleDef
from users.models import User # noqa: TC001

from kausal_common.graphene import GQLInfo
# Instead of defining a new class for the Strawberry type UserFrameworkRole and copying the fields of FrameworkRoleDef,
# register FrameworkRoleDef with a different name. This way we don't need to juggle data around in
# `UserType.framework_roles()`.
strawberry.type(FrameworkRoleDef, name='UserFrameworkRole')
register_strawberry_type(FrameworkRoleDef)

from frameworks.roles import FrameworkRoleDef

@register_strawberry_type
@strawberry.type
class UserType:
id: int
email: str
first_name: str
last_name: str

class UserFrameworkRole(graphene.ObjectType):
framework_id = graphene.ID(required=True)
role_id = graphene.String(required=False)
org_slug = graphene.String(required=False)
org_id = graphene.String(required=False)
_user: strawberry.Private[User]

def __init__(self, user: User):
proper_fields = [

Check warning on line 32 in users/schema.py

View check run for this annotation

Kausal Code Coverage / codecov/patch

users/schema.py#L32

Added line #L32 was not covered by tests
field.name for field in dataclasses.fields(self)
if not isinstance(field, StrawberryField) and field.name != '_user'
]
for field in proper_fields:
setattr(self, field, getattr(user, field))
self._user = user

Check warning on line 38 in users/schema.py

View check run for this annotation

Kausal Code Coverage / codecov/patch

users/schema.py#L37-L38

Added lines #L37 - L38 were not covered by tests

class UserType(DjangoObjectType):
framework_roles = graphene.List(graphene.NonNull(UserFrameworkRole))

class Meta:
model = User
fields = ('id', 'email', 'first_name', 'last_name')

@staticmethod
def resolve_framework_roles(root: User, info: GQLInfo) -> Sequence[FrameworkRoleDef]:
return root.extra.framework_roles
@strawberry.field
def framework_roles(self) -> list[FrameworkRoleDef]:
return list(self._user.extra.framework_roles)

Check warning on line 42 in users/schema.py

View check run for this annotation

Kausal Code Coverage / codecov/patch

users/schema.py#L42

Added line #L42 was not covered by tests


class Query(graphene.ObjectType):
me = graphene.Field(UserType)

def resolve_me(self, info):
def resolve_me(self, info) -> UserType | None:
user = info.context.user
if user.is_authenticated:
return user
return UserType(user)

Check warning on line 51 in users/schema.py

View check run for this annotation

Kausal Code Coverage / codecov/patch

users/schema.py#L51

Added line #L51 was not covered by tests
return None


class RegisterUser(graphene.Mutation):
class Arguments:
email = graphene.String(required=True)
password = graphene.String(required=True)

user = graphene.Field(UserType)

def mutate(self, info: GQLInfo, email: str, password: str):
email = email.strip().lower()
if User.objects.filter(email=email).exists():
raise GraphQLError("User with email already exists", nodes=info.field_nodes)
user = User(email=email)
user.set_password(password)
user.save()
return RegisterUser(user=user)


class Mutations(graphene.ObjectType):
register_user = RegisterUser.Field()