From a23dd113540d4aa3d530cf50e0106f15acf22050 Mon Sep 17 00:00:00 2001 From: =Awais Jibran Date: Thu, 6 Jan 2022 20:28:44 +0500 Subject: [PATCH 1/3] test: add more tests Add more tests and convert tests to variables than hardcoded string. --- scheduler/api/tests.py | 76 +++++++++++++++++++++++---- scheduler/meeting_scheduler/schema.py | 6 +-- 2 files changed, 68 insertions(+), 14 deletions(-) diff --git a/scheduler/api/tests.py b/scheduler/api/tests.py index 191635fd..84f63877 100644 --- a/scheduler/api/tests.py +++ b/scheduler/api/tests.py @@ -11,6 +11,7 @@ class BookingAPITests(BaseTests): """ Booking api tests. """ + def setUp(self) -> None: self.user = self.create_user(username="api-user") self.availability = self.create_availability(self.user) @@ -20,9 +21,10 @@ def setUp(self) -> None: start_time=time(hour=11, minute=0, second=0), total_time=15 ) + self.booking_by_user_query = ''' - query getUserBookings { - bookingsByUser(username: "api-user"){ + query getUserBookings($username: String!) { + bookingsByUser(username: $username){ id user { id username email } @@ -30,17 +32,44 @@ def setUp(self) -> None: } ''' + @classmethod + def execute_and_assert_success(cls, query, **kwargs): + """ + Run the query and assert there were no errors. + """ + result = schema.execute(query, **kwargs) + + assert result.errors is None, result.errors + return result.data + + @classmethod + def execute_and_assert_error(cls, query, error, **kwargs): + """ + Run the query and assert there the expected error is raised. + """ + result = schema.execute(query, **kwargs) + assert result.errors is not None, "No errors while executing query!" + assert any( + [error in err.message for err in result.errors] + ) is True, f'No error {error} instead {result.errors}' + return result.errors + def test_user_has_one_booking(self): """Test that get user booking api returns data.""" - result = schema.execute(self.booking_by_user_query) - data = result.data + data = self.execute_and_assert_success( + self.booking_by_user_query, + variables={"username": "api-user"} + ) + assert data is not None assert len(data['bookingsByUser']) == 1 def test_user_booking_fields(self): """Test that get user booking api returns expected data.""" - result = schema.execute(self.booking_by_user_query) - booking = result.data['bookingsByUser'][0] + booking = self.execute_and_assert_success( + self.booking_by_user_query, + variables={"username": "api-user"} + )['bookingsByUser'][0] assert booking['id'] == f'{self.user_booking.id}' assert booking['user'] == {'id': f'{self.user.id}', 'username': 'api-user', 'email': self.user.email} @@ -50,9 +79,9 @@ def test_user_booking_additional_fields(self): Tests that you can provide additional api key fields and the api returns those additional fields. """ - self.booking_by_user_query = ''' - query getUserBookings { - bookingsByUser(username: "api-user"){ + query = ''' + query getUserBookings($username: String!) { + bookingsByUser(username: $username){ id fullName email date startTime endTime totalTime updatedAt user { id username email @@ -60,7 +89,32 @@ def test_user_booking_additional_fields(self): } } ''' - result = schema.execute(self.booking_by_user_query) - booking = result.data['bookingsByUser'][0] + booking = self.execute_and_assert_success( + query, + variables={"username": "api-user"} + )['bookingsByUser'][0] + for field in "id fullName email date startTime endTime totalTime updatedAt".split(): assert field in booking + + def test_variable_error(self): + """""" + query = ''' + query getUserBookings($username: String) { + bookingsByUser(username: $username){ + id fullName email date startTime endTime totalTime updatedAt + user { + id username email + } + } + } + ''' + expected_error = 'Variable "username" of type "String" used in position expecting type "String!".' + self.execute_and_assert_error(query=query, variables={"username": "api-user"}, error=expected_error) + + def test_missing_variable(self): + """ + Test that missing variable error is raised when variable is not provided. + """ + expected_error = 'Variable "$username" of required type "String!" was not provided.' + self.execute_and_assert_error(self.booking_by_user_query, error=expected_error) diff --git a/scheduler/meeting_scheduler/schema.py b/scheduler/meeting_scheduler/schema.py index f988fa96..a4651f6b 100644 --- a/scheduler/meeting_scheduler/schema.py +++ b/scheduler/meeting_scheduler/schema.py @@ -21,9 +21,9 @@ class Query(UserQuery, graphene.ObjectType): bookings_by_user = graphene.List( BookingNode, - username=graphene.Argument( - graphene.String, description="Pass username of the user.", required=True - ), + username=graphene.String(required=True), + # Alternative + # username=graphene.Argument(graphene.String, description="Pass username of the user.", required=True), ) @classmethod From 9de1c654777e3c3639b04a8fa6ae1efce8558e03 Mon Sep 17 00:00:00 2001 From: =Awais Jibran Date: Thu, 6 Jan 2022 20:46:27 +0500 Subject: [PATCH 2/3] refactor: restructure and cleanup code for api schema.py --- scheduler/api/schema.py | 17 ++++++++ scheduler/api/tests.py | 2 +- scheduler/api/urls.py | 3 +- scheduler/meeting_scheduler/admin.py | 3 +- scheduler/meeting_scheduler/mutations.py | 8 ++-- scheduler/meeting_scheduler/nodes.py | 2 +- scheduler/meeting_scheduler/schema.py | 55 ++++++++++++++---------- scheduler/meeting_scheduler/tests.py | 2 +- 8 files changed, 59 insertions(+), 33 deletions(-) create mode 100644 scheduler/api/schema.py diff --git a/scheduler/api/schema.py b/scheduler/api/schema.py new file mode 100644 index 00000000..0f148fdf --- /dev/null +++ b/scheduler/api/schema.py @@ -0,0 +1,17 @@ +import graphene +from graphql_auth.schema import UserQuery + +from scheduler.meeting_scheduler.schema import ( + AvailabilityQuery, BookingQuery, AvailabilityMutation, BookingMutation, UserMutation +) + + +class Query(BookingQuery, AvailabilityQuery, UserQuery): + pass + + +class Mutation(AvailabilityMutation, BookingMutation, UserMutation): + pass + + +schema = graphene.Schema(query=Query, mutation=Mutation) diff --git a/scheduler/api/tests.py b/scheduler/api/tests.py index 84f63877..0c76a808 100644 --- a/scheduler/api/tests.py +++ b/scheduler/api/tests.py @@ -3,7 +3,7 @@ """ from datetime import time -from scheduler.meeting_scheduler.schema import schema +from .schema import schema from scheduler.meeting_scheduler.tests import BaseTests diff --git a/scheduler/api/urls.py b/scheduler/api/urls.py index c4de0aac..cf89484c 100644 --- a/scheduler/api/urls.py +++ b/scheduler/api/urls.py @@ -5,9 +5,8 @@ from django.views.decorators.csrf import csrf_exempt from graphene_django.views import GraphQLView -from scheduler.meeting_scheduler.schema import schema +from .schema import schema urlpatterns = [ path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))), - ] diff --git a/scheduler/meeting_scheduler/admin.py b/scheduler/meeting_scheduler/admin.py index 603991d0..435a02c8 100644 --- a/scheduler/meeting_scheduler/admin.py +++ b/scheduler/meeting_scheduler/admin.py @@ -5,11 +5,12 @@ from django.contrib import admin from django.contrib.sessions.models import Session -from scheduler.meeting_scheduler.models import Booking, Availability, UserModel as User +from .models import Booking, Availability, UserModel as User class SessionAdmin(admin.ModelAdmin): """Django session model admin """ + def _session_data(self, obj): """Return decoded session data.""" return obj.get_decoded() diff --git a/scheduler/meeting_scheduler/mutations.py b/scheduler/meeting_scheduler/mutations.py index e432a02a..c8c09d07 100644 --- a/scheduler/meeting_scheduler/mutations.py +++ b/scheduler/meeting_scheduler/mutations.py @@ -5,10 +5,10 @@ import graphene from graphql import GraphQLError -from scheduler.meeting_scheduler.decorators import user_required -from scheduler.meeting_scheduler.enums import Description -from scheduler.meeting_scheduler.models import Booking, UserModel as User, Availability -from scheduler.meeting_scheduler.nodes import BookingNode, AvailabilityNode +from .decorators import user_required +from .enums import Description +from .models import Booking, UserModel as User, Availability +from .nodes import BookingNode, AvailabilityNode class CreateBooking(graphene.Mutation): diff --git a/scheduler/meeting_scheduler/nodes.py b/scheduler/meeting_scheduler/nodes.py index 1eba1afe..6d230120 100644 --- a/scheduler/meeting_scheduler/nodes.py +++ b/scheduler/meeting_scheduler/nodes.py @@ -5,7 +5,7 @@ import graphene from graphene_django import DjangoObjectType -from scheduler.meeting_scheduler.models import Booking, Availability, UserModel +from .models import Booking, Availability, UserModel class UserType(DjangoObjectType): diff --git a/scheduler/meeting_scheduler/schema.py b/scheduler/meeting_scheduler/schema.py index a4651f6b..8201cf62 100644 --- a/scheduler/meeting_scheduler/schema.py +++ b/scheduler/meeting_scheduler/schema.py @@ -3,22 +3,17 @@ from graphql_auth.schema import UserQuery from graphql_jwt.decorators import user_passes_test -from scheduler.meeting_scheduler.models import Booking, Availability -from scheduler.meeting_scheduler.mutations import ( +from .models import Booking, Availability +from .mutations import ( CreateBooking, CreateAvailability, DeleteAvailability, UpdateAvailability, ) -from scheduler.meeting_scheduler.nodes import AvailabilityNode, BookingNode +from .nodes import AvailabilityNode, BookingNode -class Query(UserQuery, graphene.ObjectType): +class BookingQuery(graphene.ObjectType): """ - Describes entry point for fields to *read* data in the booking Schema. + Describes entry point for fields to *read* data in the booking schema. """ - availabilities = graphene.List(AvailabilityNode) - availability = graphene.Field(AvailabilityNode, id=graphene.Int( - required=True, description="ID of a availability to view" - )) - bookings_by_user = graphene.List( BookingNode, username=graphene.String(required=True), @@ -26,6 +21,21 @@ class Query(UserQuery, graphene.ObjectType): # username=graphene.Argument(graphene.String, description="Pass username of the user.", required=True), ) + @classmethod + def resolve_bookings_by_user(cls, root, info, username): + """Resolve bookings by user""" + return Booking.objects.filter(user__username=username).prefetch_related('user') + + +class AvailabilityQuery(UserQuery, graphene.ObjectType): + """ + Describes entry point for fields to *read* data in the availability schema. + """ + availabilities = graphene.List(AvailabilityNode) + availability = graphene.Field(AvailabilityNode, id=graphene.Int( + required=True, description="ID of a availability to view" + )) + @classmethod @user_passes_test(lambda user: user and not user.is_anonymous) def resolve_availabilities(cls, root, info): @@ -38,27 +48,26 @@ def resolve_availability(cls, root, info, id): """Resolve the user availability field""" return Availability.objects.get(id=id, user=info.context.user) - @classmethod - def resolve_bookings_by_user(cls, root, info, username): - """Resolve bookings by user""" - return Booking.objects.filter(user__username=username).prefetch_related('user') + +class BookingMutation(graphene.ObjectType): + """ + Describes entry point for fields to *create* data in bookings API. + """ + create_booking = CreateBooking.Field() -class Mutation(graphene.ObjectType): +class AvailabilityMutation(graphene.ObjectType): """ - Describes entry point for fields to *create, update or delete* data in bookings API. + Describes entry point for fields to *create, update or delete* data in availability API. """ - # Availability mutations create_availability = CreateAvailability.Field() update_availability = UpdateAvailability.Field() delete_availability = DeleteAvailability.Field() - # Booking mutations - create_booking = CreateBooking.Field() - # User mutations +class UserMutation(graphene.ObjectType): + """ + Describes entry point for fields to *login, verify token* data in user API. + """ login = mutations.ObtainJSONWebToken.Field(description="Login and obtain token for the user") verify_token = mutations.VerifyToken.Field(description="Verify if the token is valid.") - - -schema = graphene.Schema(query=Query, mutation=Mutation) diff --git a/scheduler/meeting_scheduler/tests.py b/scheduler/meeting_scheduler/tests.py index b875042c..2aedda6f 100644 --- a/scheduler/meeting_scheduler/tests.py +++ b/scheduler/meeting_scheduler/tests.py @@ -4,7 +4,7 @@ from django.test import TestCase -from scheduler.meeting_scheduler.models import Booking, UserModel, Availability +from .models import Booking, UserModel, Availability class BaseTests(TestCase): From 98fa6e853fa1a086de87b0b891381c939d7b5e98 Mon Sep 17 00:00:00 2001 From: =Awais Jibran Date: Mon, 17 Jan 2022 18:37:03 +0500 Subject: [PATCH 3/3] refactor: Sevaral refactoring in the app. 1. Use Django sites. 2. Use django site model in the admin 3. Update docs. --- ReadMe.md | 24 +++++++++++++------ scheduler/meeting_scheduler/mutations.py | 8 +++---- scheduler/meeting_scheduler/schema.py | 8 +++---- .../meeting_scheduler/{nodes.py => types.py} | 5 ++-- scheduler/settings.py | 4 ++++ 5 files changed, 31 insertions(+), 18 deletions(-) rename scheduler/meeting_scheduler/{nodes.py => types.py} (91%) diff --git a/ReadMe.md b/ReadMe.md index ba91636c..aab7457b 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,10 +1,20 @@ -# Django Graphene (GraphQL) API +# Schedule Booking GraphQL API + +Schedule Booking app is powered by a GraphQL API. GraphQL is a query language that allows clients to talk to an API server. Unlike REST, it gives the client control over how much or how little data they want to request about each object and allows relations within the object graph to be traversed easily. + +To learn more about GraphQL language and its concepts, see the official [GraphQL website](https://graphql.org/). + +The API endpoint is available at `/graphql/` and requires queries to be submitted using HTTP `POST` method and the `application/json` content type. + +The API provides simple CURD operation. The application has simple data flow where +authenticated users can create their availabilities slots and other users can +book these slots. Other uses can also see all the bookings of a user. +CURD operations are provided on authenticated availability endpoint using `JWT` authentication +mechanism. API provides both types of operations: + +* Public (search & book slots in availability.) +* Private (create/update/delete availabilities) -This is simple application which provided CURD operation. The application has simple -data flow where authenticated users can create their availabilities slots and -other users can book these slots. Other uses can also see all the bookings of a user. -CURD ops are provided on authenticated availability endpoint using `JWT` authentication -mechanism. **Project Requirements:** @@ -22,7 +32,7 @@ testing GraphQL queries. Follow the step by step guide below to run & test the G Screen Shot 2021-12-24 at 3 25 55 AM -### Getting Started +### Development Setup This project is created and tested with `Python 3.8.10` #### Create & activate virtual environment. diff --git a/scheduler/meeting_scheduler/mutations.py b/scheduler/meeting_scheduler/mutations.py index c8c09d07..ba799158 100644 --- a/scheduler/meeting_scheduler/mutations.py +++ b/scheduler/meeting_scheduler/mutations.py @@ -8,14 +8,14 @@ from .decorators import user_required from .enums import Description from .models import Booking, UserModel as User, Availability -from .nodes import BookingNode, AvailabilityNode +from .types import BookingType, AvailabilityType class CreateBooking(graphene.Mutation): """ OTD mutation class for creating bookings with users. """ - booking = graphene.Field(BookingNode) + booking = graphene.Field(BookingType) success = graphene.Boolean() class Arguments: @@ -50,7 +50,7 @@ class CreateAvailability(graphene.Mutation): """ OTD mutation class for creating user availabilities. """ - availability = graphene.Field(AvailabilityNode) + availability = graphene.Field(AvailabilityType) success = graphene.Boolean() error = graphene.String() @@ -78,7 +78,7 @@ class UpdateAvailability(graphene.Mutation): """ OTD mutation class for updating user availabilities. """ - availability = graphene.Field(AvailabilityNode) + availability = graphene.Field(AvailabilityType) success = graphene.Boolean() error = graphene.String() diff --git a/scheduler/meeting_scheduler/schema.py b/scheduler/meeting_scheduler/schema.py index 8201cf62..f86a8616 100644 --- a/scheduler/meeting_scheduler/schema.py +++ b/scheduler/meeting_scheduler/schema.py @@ -7,7 +7,7 @@ from .mutations import ( CreateBooking, CreateAvailability, DeleteAvailability, UpdateAvailability, ) -from .nodes import AvailabilityNode, BookingNode +from .types import AvailabilityType, BookingType class BookingQuery(graphene.ObjectType): @@ -15,7 +15,7 @@ class BookingQuery(graphene.ObjectType): Describes entry point for fields to *read* data in the booking schema. """ bookings_by_user = graphene.List( - BookingNode, + BookingType, username=graphene.String(required=True), # Alternative # username=graphene.Argument(graphene.String, description="Pass username of the user.", required=True), @@ -31,8 +31,8 @@ class AvailabilityQuery(UserQuery, graphene.ObjectType): """ Describes entry point for fields to *read* data in the availability schema. """ - availabilities = graphene.List(AvailabilityNode) - availability = graphene.Field(AvailabilityNode, id=graphene.Int( + availabilities = graphene.List(AvailabilityType) + availability = graphene.Field(AvailabilityType, id=graphene.Int( required=True, description="ID of a availability to view" )) diff --git a/scheduler/meeting_scheduler/nodes.py b/scheduler/meeting_scheduler/types.py similarity index 91% rename from scheduler/meeting_scheduler/nodes.py rename to scheduler/meeting_scheduler/types.py index 6d230120..b8d586e8 100644 --- a/scheduler/meeting_scheduler/nodes.py +++ b/scheduler/meeting_scheduler/types.py @@ -1,7 +1,6 @@ """ Custom scheduler app nodes """ - import graphene from graphene_django import DjangoObjectType @@ -16,7 +15,7 @@ class Meta: fields = ("id", "username", "email") -class AvailabilityNode(DjangoObjectType): +class AvailabilityType(DjangoObjectType): """Availability Object Type Definition""" id = graphene.ID() interval_mints = graphene.String() @@ -31,7 +30,7 @@ def resolve_interval_mints(cls, availability, info): return availability.get_interval_mints_display() -class BookingNode(DjangoObjectType): +class BookingType(DjangoObjectType): """Booking Object Type Definition""" id = graphene.ID() user = graphene.Field(UserType) diff --git a/scheduler/settings.py b/scheduler/settings.py index cd2eb278..41bf8cba 100644 --- a/scheduler/settings.py +++ b/scheduler/settings.py @@ -39,8 +39,11 @@ 'graphql_jwt.refresh_token.apps.RefreshTokenConfig', 'graphql_auth', 'django_filters', + 'django.contrib.sites', ] +SITE_ID = 1 + GRAPHENE = { "MIDDLEWARE": [ "graphql_jwt.middleware.JSONWebTokenMiddleware", @@ -65,6 +68,7 @@ } MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sites.middleware.CurrentSiteMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',