mirror of
				https://github.com/graphql-python/graphene-django.git
				synced 2025-10-31 07:57:31 +03:00 
			
		
		
		
	Merge remote-tracking branch 'remote/master' into recursive-nodes
This commit is contained in:
		
						commit
						57dbcd34f4
					
				
							
								
								
									
										13
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							|  | @ -65,3 +65,16 @@ target/ | |||
| # Databases | ||||
| *.sqlite3 | ||||
| .vscode | ||||
| 
 | ||||
| # swap | ||||
| [._]*.s[a-v][a-z] | ||||
| [._]*.sw[a-p] | ||||
| [._]s[a-v][a-z] | ||||
| [._]sw[a-p] | ||||
| # session | ||||
| Session.vim | ||||
| # temporary | ||||
| .netrwhist | ||||
| *~ | ||||
| # auto-generated tag files | ||||
| tags | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| Authorization in Django | ||||
| ======================= | ||||
| 
 | ||||
| There are two main ways you may want to limit access to data when | ||||
| There are several ways you may want to limit access to data when | ||||
| working with Graphene and Django: limiting which fields are accessible | ||||
| via GraphQL and limiting which objects a user can access. | ||||
| 
 | ||||
|  | @ -34,6 +34,20 @@ This is easy, simply use the ``only_fields`` meta attribute. | |||
|             only_fields = ('title', 'content') | ||||
|             interfaces = (relay.Node, ) | ||||
| 
 | ||||
| conversely you can use ``exclude_fields`` meta atrribute. | ||||
| 
 | ||||
| .. code:: python | ||||
| 
 | ||||
|     from graphene import relay | ||||
|     from graphene_django.types import DjangoObjectType | ||||
|     from .models import Post | ||||
| 
 | ||||
|     class PostNode(DjangoObjectType): | ||||
|         class Meta: | ||||
|             model = Post | ||||
|             exclude_fields = ('published', 'owner') | ||||
|             interfaces = (relay.Node, ) | ||||
| 
 | ||||
| Queryset Filtering On Lists | ||||
| --------------------------- | ||||
| 
 | ||||
|  | @ -108,3 +122,28 @@ method to your ``DjangoObjectType``. | |||
|             if post.published or context.user == post.owner: | ||||
|                 return post | ||||
|             return None | ||||
| 
 | ||||
| Adding login required | ||||
| --------------------- | ||||
| 
 | ||||
| If you want to use the standard Django LoginRequiredMixin_ you can create your own view, which includes the ``LoginRequiredMixin`` and subclasses the ``GraphQLView``: | ||||
| 
 | ||||
| .. code:: python | ||||
| 
 | ||||
|     from django.contrib.auth.mixins import LoginRequiredMixin | ||||
|     from graphene_django.views import GraphQLView | ||||
| 
 | ||||
| 
 | ||||
|     class PrivateGraphQLView(LoginRequiredMixin, GraphQLView): | ||||
|         pass | ||||
| 
 | ||||
| After this, you can use the new ``PrivateGraphQLView`` in ``urls.py``: | ||||
| 
 | ||||
| .. code:: python | ||||
| 
 | ||||
|     urlpatterns = [ | ||||
|       # some other urls | ||||
|       url(r'^graphql', PrivateGraphQLView.as_view(graphiql=True, schema=schema)), | ||||
|     ] | ||||
| 
 | ||||
| .. _LoginRequiredMixin: https://docs.djangoproject.com/en/1.10/topics/auth/default/#the-loginrequired-mixin | ||||
|  |  | |||
|  | @ -63,7 +63,7 @@ master_doc = 'index' | |||
| 
 | ||||
| # General information about the project. | ||||
| project = u'Graphene Django' | ||||
| copyright = u'Graphene 2016' | ||||
| copyright = u'Graphene 2017' | ||||
| author = u'Syrus Akbary' | ||||
| 
 | ||||
| # The version info for the project you're documenting, acts as replacement for | ||||
|  |  | |||
|  | @ -6,7 +6,8 @@ Contents: | |||
| .. toctree:: | ||||
|    :maxdepth: 0 | ||||
| 
 | ||||
|    tutorial | ||||
|    tutorial-plain | ||||
|    tutorial-relay | ||||
|    filtering | ||||
|    authorization | ||||
|    debug | ||||
|  |  | |||
							
								
								
									
										516
									
								
								docs/tutorial-plain.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										516
									
								
								docs/tutorial-plain.rst
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,516 @@ | |||
| Introduction tutorial - Graphene and Django | ||||
| =========================================== | ||||
| 
 | ||||
| Graphene has a number of additional features that are designed to make | ||||
| working with Django *really simple*. | ||||
| 
 | ||||
| Our primary focus here is to give a good understanding of how to connect models from Django ORM to graphene object types. | ||||
| 
 | ||||
| A good idea is to check the `graphene <http://docs.graphene-python.org/en/latest/>`__ documentation first. | ||||
| 
 | ||||
| Setup the Django project | ||||
| ------------------------ | ||||
| 
 | ||||
| You can find the entire project in ``examples/cookbook-plain``. | ||||
| 
 | ||||
| ---- | ||||
| 
 | ||||
| We will setup the project, create the following: | ||||
| 
 | ||||
| -  A Django project called ``cookbook`` | ||||
| -  An app within ``cookbook`` called ``ingredients`` | ||||
| 
 | ||||
| .. code:: bash | ||||
| 
 | ||||
|     # Create the project directory | ||||
|     mkdir cookbook | ||||
|     cd cookbook | ||||
| 
 | ||||
|     # Create a virtualenv to isolate our package dependencies locally | ||||
|     virtualenv env | ||||
|     source env/bin/activate  # On Windows use `env\Scripts\activate` | ||||
| 
 | ||||
|     # Install Django and Graphene with Django support | ||||
|     pip install django | ||||
|     pip install graphene_django | ||||
| 
 | ||||
|     # Set up a new project with a single application | ||||
|     django-admin.py startproject cookbook .  # Note the trailing '.' character | ||||
|     cd cookbook | ||||
|     django-admin.py startapp ingredients | ||||
| 
 | ||||
| Now sync your database for the first time: | ||||
| 
 | ||||
| .. code:: bash | ||||
| 
 | ||||
|     python manage.py migrate | ||||
| 
 | ||||
| Let's create a few simple models... | ||||
| 
 | ||||
| Defining our models | ||||
| ^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Let's get started with these models: | ||||
| 
 | ||||
| .. code:: python | ||||
| 
 | ||||
|     # cookbook/ingredients/models.py | ||||
|     from django.db import models | ||||
| 
 | ||||
| 
 | ||||
|     class Category(models.Model): | ||||
|         name = models.CharField(max_length=100) | ||||
| 
 | ||||
|         def __str__(self): | ||||
|             return self.name | ||||
| 
 | ||||
| 
 | ||||
|     class Ingredient(models.Model): | ||||
|         name = models.CharField(max_length=100) | ||||
|         notes = models.TextField() | ||||
|         category = models.ForeignKey(Category, related_name='ingredients') | ||||
| 
 | ||||
|         def __str__(self): | ||||
|             return self.name | ||||
| 
 | ||||
| Don't forget to create & run migrations: | ||||
| 
 | ||||
| .. code:: bash | ||||
| 
 | ||||
|     python manage.py makemigrations | ||||
|     python manage.py migrate | ||||
| 
 | ||||
| Load some test data | ||||
| ^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Now is a good time to load up some test data. The easiest option will be | ||||
| to `download the | ||||
| ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__ | ||||
| fixture and place it in | ||||
| ``cookbook/ingredients/fixtures/ingredients.json``. You can then run the | ||||
| following: | ||||
| 
 | ||||
| .. code:: bash | ||||
| 
 | ||||
|     $ python ./manage.py loaddata ingredients | ||||
| 
 | ||||
|     Installed 6 object(s) from 1 fixture(s) | ||||
| 
 | ||||
| Alternatively you can use the Django admin interface to create some data | ||||
| yourself. You'll need to run the development server (see below), and | ||||
| create a login for yourself too (``./manage.py createsuperuser``). | ||||
| 
 | ||||
| Hello GraphQL - Schema and Object Types | ||||
| --------------------------------------- | ||||
| 
 | ||||
| In order to make queries to our Django project, we are going to need few things: | ||||
| 
 | ||||
| * Schema with defined object types | ||||
| * A view, taking queries as input and returning the result | ||||
| 
 | ||||
| GraphQL presents your objects to the world as a graph structure rather | ||||
| than a more hierarchical structure to which you may be accustomed. In | ||||
| order to create this representation, Graphene needs to know about each | ||||
| *type* of object which will appear in the graph. | ||||
| 
 | ||||
| This graph also has a *root type* through which all access begins. This | ||||
| is the ``Query`` class below. | ||||
| 
 | ||||
| This means, for each of our models, we are going to create a type, subclassing ``DjangoObjectType`` | ||||
| 
 | ||||
| After we've done that, we will list those types as fields in the ``Query`` class. | ||||
| 
 | ||||
| Create ``cookbook/ingredients/schema.py`` and type the following: | ||||
| 
 | ||||
| .. code:: python | ||||
| 
 | ||||
|     # cookbook/ingredients/schema.py | ||||
|     import graphene | ||||
| 
 | ||||
|     from graphene_django.types import DjangoObjectType | ||||
| 
 | ||||
|     from cookbook.ingredients.models import Category, Ingredient | ||||
| 
 | ||||
| 
 | ||||
|     class CategoryType(DjangoObjectType): | ||||
|         class Meta: | ||||
|             model = Category | ||||
| 
 | ||||
| 
 | ||||
|     class IngredientType(DjangoObjectType): | ||||
|         class Meta: | ||||
|             model = Ingredient | ||||
| 
 | ||||
| 
 | ||||
|     class Query(graphene.AbstractType): | ||||
|         all_categories = graphene.List(CategoryType) | ||||
|         all_ingredients = graphene.List(IngredientType) | ||||
| 
 | ||||
|         def resolve_all_categories(self, args, context, info): | ||||
|             return Category.objects.all() | ||||
| 
 | ||||
|         def resolve_all_ingredients(self, args, context, info): | ||||
|             # We can easily optimize query count in the resolve method | ||||
|             return Ingredient.objects.select_related('category').all() | ||||
| 
 | ||||
| 
 | ||||
| Note that the above ``Query`` class is marked as 'abstract'. This is | ||||
| because we will now create a project-level query which will combine all | ||||
| our app-level queries. | ||||
| 
 | ||||
| Create the parent project-level ``cookbook/schema.py``: | ||||
| 
 | ||||
| .. code:: python | ||||
| 
 | ||||
|     import graphene | ||||
| 
 | ||||
|     import cookbook.ingredients.schema | ||||
| 
 | ||||
| 
 | ||||
|     class Query(cookbook.ingredients.schema.Query, graphene.ObjectType): | ||||
|         # This class will inherit from multiple Queries | ||||
|         # as we begin to add more apps to our project | ||||
|         pass | ||||
| 
 | ||||
|     schema = graphene.Schema(query=Query) | ||||
| 
 | ||||
| You can think of this as being something like your top-level ``urls.py`` | ||||
| file (although it currently lacks any namespacing). | ||||
| 
 | ||||
| Testing everything so far | ||||
| ------------------------- | ||||
| 
 | ||||
| We are going to do some configuration work, in order to have a working Django where we can test queries, before we move on, updating our schema. | ||||
| 
 | ||||
| Update settings | ||||
| ^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Next, install your app and GraphiQL in your Django project. GraphiQL is | ||||
| a web-based integrated development environment to assist in the writing | ||||
| and executing of GraphQL queries. It will provide us with a simple and | ||||
| easy way of testing our cookbook project. | ||||
| 
 | ||||
| Add ``ingredients`` and ``graphene_django`` to ``INSTALLED_APPS`` in ``cookbook/settings.py``: | ||||
| 
 | ||||
| .. code:: python | ||||
| 
 | ||||
|     INSTALLED_APPS = [ | ||||
|         ... | ||||
|         # This will also make the `graphql_schema` management command available | ||||
|         'graphene_django', | ||||
| 
 | ||||
|         # Install the ingredients app | ||||
|         'ingredients', | ||||
|     ] | ||||
| 
 | ||||
| And then add the ``SCHEMA`` to the ``GRAPHENE`` config in ``cookbook/settings.py``: | ||||
| 
 | ||||
| .. code:: python | ||||
| 
 | ||||
|     GRAPHENE = { | ||||
|         'SCHEMA': 'cookbook.schema.schema' | ||||
|     } | ||||
| 
 | ||||
| Alternatively, we can specify the schema to be used in the urls definition, | ||||
| as explained below. | ||||
| 
 | ||||
| Creating GraphQL and GraphiQL views | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Unlike a RESTful API, there is only a single URL from which GraphQL is | ||||
| accessed. Requests to this URL are handled by Graphene's ``GraphQLView`` | ||||
| view. | ||||
| 
 | ||||
| This view will serve as GraphQL endpoint. As we want to have the | ||||
| aforementioned GraphiQL we specify that on the parameters with ``graphiql=True``. | ||||
| 
 | ||||
| .. code:: python | ||||
| 
 | ||||
|     from django.conf.urls import url, include | ||||
|     from django.contrib import admin | ||||
| 
 | ||||
|     from graphene_django.views import GraphQLView | ||||
| 
 | ||||
|     urlpatterns = [ | ||||
|         url(r'^admin/', admin.site.urls), | ||||
|         url(r'^graphql', GraphQLView.as_view(graphiql=True)), | ||||
|     ] | ||||
| 
 | ||||
| 
 | ||||
| If we didn't specify the target schema in the Django settings file | ||||
| as explained above, we can do so here using: | ||||
| 
 | ||||
| .. code:: python | ||||
| 
 | ||||
|     from django.conf.urls import url, include | ||||
|     from django.contrib import admin | ||||
| 
 | ||||
|     from graphene_django.views import GraphQLView | ||||
| 
 | ||||
|     from cookbook.schema import schema | ||||
| 
 | ||||
|     urlpatterns = [ | ||||
|         url(r'^admin/', admin.site.urls), | ||||
|         url(r'^graphql', GraphQLView.as_view(graphiql=True, schema=schema)), | ||||
|     ] | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| Testing our GraphQL schema | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| We're now ready to test the API we've built. Let's fire up the server | ||||
| from the command line. | ||||
| 
 | ||||
| .. code:: bash | ||||
| 
 | ||||
|     $ python ./manage.py runserver | ||||
| 
 | ||||
|     Performing system checks... | ||||
|     Django version 1.9, using settings 'cookbook.settings' | ||||
|     Starting development server at http://127.0.0.1:8000/ | ||||
|     Quit the server with CONTROL-C. | ||||
| 
 | ||||
| Go to `localhost:8000/graphql <http://localhost:8000/graphql>`__ and | ||||
| type your first query! | ||||
| 
 | ||||
| .. code:: | ||||
| 
 | ||||
|     query { | ||||
|       allIngredients { | ||||
|         id | ||||
|         name | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| If you are using the provided fixtures, you will see the following response: | ||||
| 
 | ||||
| .. code:: | ||||
| 
 | ||||
|     { | ||||
|       "data": { | ||||
|         "allIngredients": [ | ||||
|           { | ||||
|             "id": "1", | ||||
|             "name": "Eggs" | ||||
|           }, | ||||
|           { | ||||
|             "id": "2", | ||||
|             "name": "Milk" | ||||
|           }, | ||||
|           { | ||||
|             "id": "3", | ||||
|             "name": "Beef" | ||||
|           }, | ||||
|           { | ||||
|             "id": "4", | ||||
|             "name": "Chicken" | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| You can experiment with ``allCategories`` too. | ||||
| 
 | ||||
| Something to have in mind is the `auto camelcasing <http://docs.graphene-python.org/en/latest/types/schema/#auto-camelcase-field-names>`__ that is happening. | ||||
| 
 | ||||
| 
 | ||||
| Getting relations | ||||
| ----------------- | ||||
| 
 | ||||
| Right now, with this simple setup in place, we can query for relations too. This is where graphql becomes really powerful! | ||||
| 
 | ||||
| For example, we may want to list all categories and in each category, all ingredients that are in that category. | ||||
| 
 | ||||
| We can do that with the following query: | ||||
| 
 | ||||
| .. code:: | ||||
| 
 | ||||
|     query { | ||||
|       allCategories { | ||||
|         id | ||||
|         name | ||||
|         ingredients { | ||||
|           id | ||||
|           name | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| This will give you (in case you are using the fixtures) the following result: | ||||
| 
 | ||||
| .. code:: | ||||
| 
 | ||||
|     { | ||||
|       "data": { | ||||
|         "allCategories": [ | ||||
|           { | ||||
|             "id": "1", | ||||
|             "name": "Dairy", | ||||
|             "ingredients": [ | ||||
|               { | ||||
|                 "id": "1", | ||||
|                 "name": "Eggs" | ||||
|               }, | ||||
|               { | ||||
|                 "id": "2", | ||||
|                 "name": "Milk" | ||||
|               } | ||||
|             ] | ||||
|           }, | ||||
|           { | ||||
|             "id": "2", | ||||
|             "name": "Meat", | ||||
|             "ingredients": [ | ||||
|               { | ||||
|                 "id": "3", | ||||
|                 "name": "Beef" | ||||
|               }, | ||||
|               { | ||||
|                 "id": "4", | ||||
|                 "name": "Chicken" | ||||
|               } | ||||
|             ] | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| We can also list all ingredients and get information for the category they are in: | ||||
| 
 | ||||
| .. code:: | ||||
| 
 | ||||
|     query { | ||||
|       allIngredients { | ||||
|         id | ||||
|         name | ||||
|         category { | ||||
|           id | ||||
|           name | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| Getting single objects | ||||
| ---------------------- | ||||
| 
 | ||||
| So far, we have been able to fetch list of objects and follow relation. But what about single objects? | ||||
| 
 | ||||
| We can update our schema to support that, by adding new query for ``ingredient`` and ``category`` and adding arguments, so we can query for specific objects. | ||||
| 
 | ||||
| .. code:: python | ||||
| 
 | ||||
|   import graphene | ||||
| 
 | ||||
|   from graphene_django.types import DjangoObjectType | ||||
| 
 | ||||
|   from cookbook.ingredients.models import Category, Ingredient | ||||
| 
 | ||||
| 
 | ||||
|   class CategoryType(DjangoObjectType): | ||||
|       class Meta: | ||||
|           model = Category | ||||
| 
 | ||||
| 
 | ||||
|   class IngredientType(DjangoObjectType): | ||||
|       class Meta: | ||||
|           model = Ingredient | ||||
| 
 | ||||
| 
 | ||||
|   class Query(graphene.AbstractType): | ||||
|       category = graphene.Field(CategoryType, | ||||
|                                 id=graphene.Int(), | ||||
|                                 name=graphene.String()) | ||||
|       all_categories = graphene.List(CategoryType) | ||||
| 
 | ||||
| 
 | ||||
|       ingredient = graphene.Field(IngredientType, | ||||
|                                   id=graphene.Int(), | ||||
|                                   name=graphene.String()) | ||||
|       all_ingredients = graphene.List(IngredientType) | ||||
| 
 | ||||
|       def resolve_all_categories(self, args, context, info): | ||||
|           return Category.objects.all() | ||||
| 
 | ||||
|       def resolve_all_ingredients(self, args, context, info): | ||||
|           return Ingredient.objects.all() | ||||
| 
 | ||||
|       def resolve_category(self, args, context, info): | ||||
|           id = args.get('id') | ||||
|           name = args.get('name') | ||||
| 
 | ||||
|           if id is not None: | ||||
|               return Category.objects.get(pk=id) | ||||
| 
 | ||||
|           if name is not None: | ||||
|               return Category.objects.get(name=name) | ||||
| 
 | ||||
|           return None | ||||
| 
 | ||||
|       def resolve_ingredient(self, args, context, info): | ||||
|           id = args.get('id') | ||||
|           name = args.get('name') | ||||
| 
 | ||||
|           if id is not None: | ||||
|               return Ingredient.objects.get(pk=id) | ||||
| 
 | ||||
|           if name is not None: | ||||
|               return Ingredient.objects.get(name=name) | ||||
| 
 | ||||
|           return None | ||||
| 
 | ||||
| Now, with the code in place, we can query for single objects. | ||||
| 
 | ||||
| For example, lets query ``category``: | ||||
| 
 | ||||
| 
 | ||||
| .. code::  | ||||
| 
 | ||||
|     query { | ||||
|       category(id: 1) { | ||||
|         name | ||||
|       } | ||||
|       anotherCategory: category(name: "Dairy") { | ||||
|         ingredients { | ||||
|           id | ||||
|           name | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| This will give us the following results: | ||||
| 
 | ||||
| .. code:: | ||||
| 
 | ||||
|     { | ||||
|       "data": { | ||||
|         "category": { | ||||
|           "name": "Dairy" | ||||
|         }, | ||||
|         "anotherCategory": { | ||||
|           "ingredients": [ | ||||
|             { | ||||
|               "id": "1", | ||||
|               "name": "Eggs" | ||||
|             }, | ||||
|             { | ||||
|               "id": "2", | ||||
|               "name": "Milk" | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| As an exercise, you can try making some queries to ``ingredient``. | ||||
| 
 | ||||
| Something to keep in mind - since we are using one field several times in our query, we need `aliases <http://graphql.org/learn/queries/#aliases>`__ | ||||
| 
 | ||||
| 
 | ||||
| Summary | ||||
| ------- | ||||
| 
 | ||||
| As you can see, GraphQL is very powerful but there are a lot of repetitions in our example. We can do a lot of improvements by adding layers of abstraction on top of ``graphene-django``. | ||||
| 
 | ||||
| If you want to put things like ``django-filter`` and automatic pagination in action, you should continue with the **relay tutorial.** | ||||
|  | @ -1,5 +1,5 @@ | |||
| Graphene-Django Tutorial | ||||
| ======================== | ||||
| Graphene and Django Tutorial using Relay | ||||
| ======================================== | ||||
| 
 | ||||
| Graphene has a number of additional features that are designed to make | ||||
| working with Django *really simple*. | ||||
|  | @ -7,6 +7,11 @@ working with Django *really simple*. | |||
| Note: The code in this quickstart is pulled from the `cookbook example | ||||
| app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__. | ||||
| 
 | ||||
| A good idea is to check the following things first: | ||||
| 
 | ||||
| * `Graphene Relay documentation <http://docs.graphene-python.org/en/latest/relay/>`__ | ||||
| * `GraphQL Relay Specification <https://facebook.github.io/relay/docs/graphql-relay-specification.html>`__ | ||||
| 
 | ||||
| Setup the Django project | ||||
| ------------------------ | ||||
| 
 | ||||
|  | @ -43,7 +48,7 @@ Now sync your database for the first time: | |||
| Let's create a few simple models... | ||||
| 
 | ||||
| Defining our models | ||||
| ------------------- | ||||
| ^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Let's get started with these models: | ||||
| 
 | ||||
|  | @ -68,6 +73,33 @@ Let's get started with these models: | |||
|         def __str__(self): | ||||
|             return self.name | ||||
| 
 | ||||
| Don't forget to create & run migrations: | ||||
| 
 | ||||
| .. code:: bash | ||||
| 
 | ||||
|     python manage.py makemigrations | ||||
|     python manage.py migrate | ||||
| 
 | ||||
| Load some test data | ||||
| ^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Now is a good time to load up some test data. The easiest option will be | ||||
| to `download the | ||||
| ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__ | ||||
| fixture and place it in | ||||
| ``cookbook/ingredients/fixtures/ingredients.json``. You can then run the | ||||
| following: | ||||
| 
 | ||||
| .. code:: bash | ||||
| 
 | ||||
|     $ python ./manage.py loaddata ingredients | ||||
| 
 | ||||
|     Installed 6 object(s) from 1 fixture(s) | ||||
| 
 | ||||
| Alternatively you can use the Django admin interface to create some data | ||||
| yourself. You'll need to run the development server (see below), and | ||||
| create a login for yourself too (``./manage.py createsuperuser``). | ||||
| 
 | ||||
| Schema | ||||
| ------ | ||||
| 
 | ||||
|  | @ -90,7 +122,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following: | |||
|     from graphene_django import DjangoObjectType | ||||
|     from graphene_django.filter import DjangoFilterConnectionField | ||||
| 
 | ||||
|     from cookbook.ingredients.models import Category, Ingredient | ||||
|     from ingredients.models import Category, Ingredient | ||||
| 
 | ||||
| 
 | ||||
|     # Graphene will automatically map the Category model's fields onto the CategoryNode. | ||||
|  | @ -145,10 +177,10 @@ Create the parent project-level ``cookbook/schema.py``: | |||
| 
 | ||||
|     import graphene | ||||
| 
 | ||||
|     import cookbook.ingredients.schema | ||||
|     import ingredients.schema | ||||
| 
 | ||||
| 
 | ||||
|     class Query(cookbook.ingredients.schema.Query, graphene.ObjectType): | ||||
|     class Query(ingredients.schema.Query, graphene.ObjectType): | ||||
|         # This class will inherit from multiple Queries | ||||
|         # as we begin to add more apps to our project | ||||
|         pass | ||||
|  | @ -158,8 +190,11 @@ Create the parent project-level ``cookbook/schema.py``: | |||
| You can think of this as being something like your top-level ``urls.py`` | ||||
| file (although it currently lacks any namespacing). | ||||
| 
 | ||||
| Testing everything so far | ||||
| ------------------------- | ||||
| 
 | ||||
| Update settings | ||||
| --------------- | ||||
| ^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Next, install your app and GraphiQL in your Django project. GraphiQL is | ||||
| a web-based integrated development environment to assist in the writing | ||||
|  | @ -191,7 +226,7 @@ Alternatively, we can specify the schema to be used in the urls definition, | |||
| as explained below. | ||||
| 
 | ||||
| Creating GraphQL and GraphiQL views | ||||
| ----------------------------------- | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Unlike a RESTful API, there is only a single URL from which GraphQL is | ||||
| accessed. Requests to this URL are handled by Graphene's ``GraphQLView`` | ||||
|  | @ -230,39 +265,9 @@ as explained above, we can do so here using: | |||
|         url(r'^graphql', GraphQLView.as_view(graphiql=True, schema=schema)), | ||||
|     ] | ||||
| 
 | ||||
| Apply model changes to database | ||||
| ------------------------------- | ||||
| 
 | ||||
| Tell Django that we've added models and update the database schema to | ||||
| reflect these additions. | ||||
| 
 | ||||
| .. code:: bash | ||||
| 
 | ||||
|     python manage.py makemigrations | ||||
|     python manage.py migrate | ||||
| 
 | ||||
| Load some test data | ||||
| ------------------- | ||||
| 
 | ||||
| Now is a good time to load up some test data. The easiest option will be | ||||
| to `download the | ||||
| ingredients.json <https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json>`__ | ||||
| fixture and place it in | ||||
| ``cookbook/ingredients/fixtures/ingredients.json``. You can then run the | ||||
| following: | ||||
| 
 | ||||
| .. code:: bash | ||||
| 
 | ||||
|     $ python ./manage.py loaddata ingredients | ||||
| 
 | ||||
|     Installed 6 object(s) from 1 fixture(s) | ||||
| 
 | ||||
| Alternatively you can use the Django admin interface to create some data | ||||
| yourself. You'll need to run the development server (see below), and | ||||
| create a login for yourself too (``./manage.py createsuperuser``). | ||||
| 
 | ||||
| Testing our GraphQL schema | ||||
| -------------------------- | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| We're now ready to test the API we've built. Let's fire up the server | ||||
| from the command line. | ||||
|  | @ -276,7 +281,7 @@ from the command line. | |||
|     Starting development server at http://127.0.0.1:8000/ | ||||
|     Quit the server with CONTROL-C. | ||||
| 
 | ||||
| Go to `localhost:8000/graphiql <http://localhost:8000/graphiql>`__ and | ||||
| Go to `localhost:8000/graphql <http://localhost:8000/graphql>`__ and | ||||
| type your first query! | ||||
| 
 | ||||
| .. code:: | ||||
							
								
								
									
										64
									
								
								examples/cookbook-plain/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								examples/cookbook-plain/README.md
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,64 @@ | |||
| Cookbook Example Django Project | ||||
| =============================== | ||||
| 
 | ||||
| This example project demos integration between Graphene and Django. | ||||
| The project contains two apps, one named `ingredients` and another | ||||
| named `recepies`. | ||||
| 
 | ||||
| Getting started | ||||
| --------------- | ||||
| 
 | ||||
| First you'll need to get the source of the project. Do this by cloning the | ||||
| whole Graphene repository: | ||||
| 
 | ||||
| ```bash | ||||
| # Get the example project code | ||||
| git clone https://github.com/graphql-python/graphene-django.git | ||||
| cd graphene-django/examples/cookbook | ||||
| ``` | ||||
| 
 | ||||
| It is good idea (but not required) to create a virtual environment | ||||
| for this project. We'll do this using | ||||
| [virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/) | ||||
| to keep things simple, | ||||
| but you may also find something like | ||||
| [virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/) | ||||
| to be useful: | ||||
| 
 | ||||
| ```bash | ||||
| # Create a virtualenv in which we can install the dependencies | ||||
| virtualenv env | ||||
| source env/bin/activate | ||||
| ``` | ||||
| 
 | ||||
| Now we can install our dependencies: | ||||
| 
 | ||||
| ```bash | ||||
| pip install -r requirements.txt | ||||
| ``` | ||||
| 
 | ||||
| Now setup our database: | ||||
| 
 | ||||
| ```bash | ||||
| # Setup the database | ||||
| ./manage.py migrate | ||||
| 
 | ||||
| # Load some example data | ||||
| ./manage.py loaddata ingredients | ||||
| 
 | ||||
| # Create an admin user (useful for logging into the admin UI | ||||
| # at http://127.0.0.1:8000/admin) | ||||
| ./manage.py createsuperuser | ||||
| ``` | ||||
| 
 | ||||
| Now you should be ready to start the server: | ||||
| 
 | ||||
| ```bash | ||||
| ./manage.py runserver | ||||
| ``` | ||||
| 
 | ||||
| Now head on over to | ||||
| [http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql) | ||||
| and run some queries! | ||||
| (See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial#testing-our-graphql-schema) | ||||
| for some example queries) | ||||
							
								
								
									
										0
									
								
								examples/cookbook-plain/cookbook/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								examples/cookbook-plain/cookbook/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										12
									
								
								examples/cookbook-plain/cookbook/ingredients/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								examples/cookbook-plain/cookbook/ingredients/admin.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| from django.contrib import admin | ||||
| 
 | ||||
| from cookbook.ingredients.models import Category, Ingredient | ||||
| 
 | ||||
| 
 | ||||
| @admin.register(Ingredient) | ||||
| class IngredientAdmin(admin.ModelAdmin): | ||||
|     list_display = ('id', 'name', 'category') | ||||
|     list_editable = ('name', 'category') | ||||
| 
 | ||||
| 
 | ||||
| admin.site.register(Category) | ||||
							
								
								
									
										7
									
								
								examples/cookbook-plain/cookbook/ingredients/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								examples/cookbook-plain/cookbook/ingredients/apps.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| from django.apps import AppConfig | ||||
| 
 | ||||
| 
 | ||||
| class IngredientsConfig(AppConfig): | ||||
|     name = 'cookbook.ingredients' | ||||
|     label = 'ingredients' | ||||
|     verbose_name = 'Ingredients' | ||||
|  | @ -0,0 +1 @@ | |||
| [{"model": "ingredients.category", "pk": 1, "fields": {"name": "Dairy"}}, {"model": "ingredients.category", "pk": 2, "fields": {"name": "Meat"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Eggs", "notes": "Good old eggs", "category": 1}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Milk", "notes": "Comes from a cow", "category": 1}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Beef", "notes": "Much like milk, this comes from a cow", "category": 2}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Chicken", "notes": "Definitely doesn't come from a cow", "category": 2}}] | ||||
|  | @ -0,0 +1,33 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.9 on 2015-12-04 18:15 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     initial = True | ||||
| 
 | ||||
|     dependencies = [ | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='Category', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('name', models.CharField(max_length=100)), | ||||
|             ], | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='Ingredient', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('name', models.CharField(max_length=100)), | ||||
|                 ('notes', models.TextField()), | ||||
|                 ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='ingredients.Category')), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
|  | @ -0,0 +1,20 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.9 on 2016-11-04 00:50 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('ingredients', '0001_initial'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='ingredient', | ||||
|             name='notes', | ||||
|             field=models.TextField(blank=True, null=True), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										17
									
								
								examples/cookbook-plain/cookbook/ingredients/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								examples/cookbook-plain/cookbook/ingredients/models.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | |||
| from django.db import models | ||||
| 
 | ||||
| 
 | ||||
| class Category(models.Model): | ||||
|     name = models.CharField(max_length=100) | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
| 
 | ||||
| 
 | ||||
| class Ingredient(models.Model): | ||||
|     name = models.CharField(max_length=100) | ||||
|     notes = models.TextField(null=True, blank=True) | ||||
|     category = models.ForeignKey(Category, related_name='ingredients') | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
							
								
								
									
										57
									
								
								examples/cookbook-plain/cookbook/ingredients/schema.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								examples/cookbook-plain/cookbook/ingredients/schema.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,57 @@ | |||
| import graphene | ||||
| from graphene_django.types import DjangoObjectType | ||||
| 
 | ||||
| from cookbook.ingredients.models import Category, Ingredient | ||||
| 
 | ||||
| 
 | ||||
| class CategoryType(DjangoObjectType): | ||||
|     class Meta: | ||||
|         model = Category | ||||
| 
 | ||||
| 
 | ||||
| class IngredientType(DjangoObjectType): | ||||
|     class Meta: | ||||
|         model = Ingredient | ||||
| 
 | ||||
| 
 | ||||
| class Query(graphene.AbstractType): | ||||
|     category = graphene.Field(CategoryType, | ||||
|                               id=graphene.Int(), | ||||
|                               name=graphene.String()) | ||||
|     all_categories = graphene.List(CategoryType) | ||||
| 
 | ||||
|     ingredient = graphene.Field(IngredientType, | ||||
|                                 id=graphene.Int(), | ||||
|                                 name=graphene.String()) | ||||
|     all_ingredients = graphene.List(IngredientType) | ||||
| 
 | ||||
|     def resolve_all_categories(self, args, context, info): | ||||
|         return Category.objects.all() | ||||
| 
 | ||||
|     def resolve_all_ingredients(self, args, context, info): | ||||
|         # We can easily optimize query count in the resolve method | ||||
|         return Ingredient.objects.select_related('category').all() | ||||
| 
 | ||||
|     def resolve_category(self, args, context, info): | ||||
|         id = args.get('id') | ||||
|         name = args.get('name') | ||||
| 
 | ||||
|         if id is not None: | ||||
|             return Category.objects.get(pk=id) | ||||
| 
 | ||||
|         if name is not None: | ||||
|             return Category.objects.get(name=name) | ||||
| 
 | ||||
|         return None | ||||
| 
 | ||||
|     def resolve_ingredient(self, args, context, info): | ||||
|         id = args.get('id') | ||||
|         name = args.get('name') | ||||
| 
 | ||||
|         if id is not None: | ||||
|             return Ingredient.objects.get(pk=id) | ||||
| 
 | ||||
|         if name is not None: | ||||
|             return Ingredient.objects.get(name=name) | ||||
| 
 | ||||
|         return None | ||||
							
								
								
									
										12
									
								
								examples/cookbook-plain/cookbook/recipes/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								examples/cookbook-plain/cookbook/recipes/admin.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| from django.contrib import admin | ||||
| 
 | ||||
| from cookbook.recipes.models import Recipe, RecipeIngredient | ||||
| 
 | ||||
| 
 | ||||
| class RecipeIngredientInline(admin.TabularInline): | ||||
|     model = RecipeIngredient | ||||
| 
 | ||||
| 
 | ||||
| @admin.register(Recipe) | ||||
| class RecipeAdmin(admin.ModelAdmin): | ||||
|     inlines = [RecipeIngredientInline] | ||||
							
								
								
									
										7
									
								
								examples/cookbook-plain/cookbook/recipes/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								examples/cookbook-plain/cookbook/recipes/apps.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| from django.apps import AppConfig | ||||
| 
 | ||||
| 
 | ||||
| class RecipesConfig(AppConfig): | ||||
|     name = 'cookbook.recipes' | ||||
|     label = 'recipes' | ||||
|     verbose_name = 'Recipes' | ||||
|  | @ -0,0 +1,36 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.9 on 2015-12-04 18:20 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     initial = True | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('ingredients', '0001_initial'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='Recipe', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('title', models.CharField(max_length=100)), | ||||
|                 ('instructions', models.TextField()), | ||||
|             ], | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='RecipeIngredient', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('amount', models.FloatField()), | ||||
|                 ('unit', models.CharField(choices=[('kg', 'Kilograms'), ('l', 'Litres'), ('', 'Units')], max_length=20)), | ||||
|                 ('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_by', to='ingredients.Ingredient')), | ||||
|                 ('recipes', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='amounts', to='recipes.Recipe')), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
|  | @ -0,0 +1,25 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # Generated by Django 1.9 on 2016-11-04 01:06 | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('recipes', '0001_initial'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.RenameField( | ||||
|             model_name='recipeingredient', | ||||
|             old_name='recipes', | ||||
|             new_name='recipe', | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='recipeingredient', | ||||
|             name='unit', | ||||
|             field=models.CharField(choices=[(b'unit', b'Units'), (b'kg', b'Kilograms'), (b'l', b'Litres'), (b'st', b'Shots')], max_length=20), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										20
									
								
								examples/cookbook-plain/cookbook/recipes/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								examples/cookbook-plain/cookbook/recipes/models.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| from django.db import models | ||||
| 
 | ||||
| from cookbook.ingredients.models import Ingredient | ||||
| 
 | ||||
| 
 | ||||
| class Recipe(models.Model): | ||||
|     title = models.CharField(max_length=100) | ||||
|     instructions = models.TextField() | ||||
| 
 | ||||
| 
 | ||||
| class RecipeIngredient(models.Model): | ||||
|     recipe = models.ForeignKey(Recipe, related_name='amounts') | ||||
|     ingredient = models.ForeignKey(Ingredient, related_name='used_by') | ||||
|     amount = models.FloatField() | ||||
|     unit = models.CharField(max_length=20, choices=( | ||||
|         ('unit', 'Units'), | ||||
|         ('kg', 'Kilograms'), | ||||
|         ('l', 'Litres'), | ||||
|         ('st', 'Shots'), | ||||
|     )) | ||||
							
								
								
									
										52
									
								
								examples/cookbook-plain/cookbook/recipes/schema.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								examples/cookbook-plain/cookbook/recipes/schema.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,52 @@ | |||
| import graphene | ||||
| from graphene_django.types import DjangoObjectType | ||||
| 
 | ||||
| from cookbook.recipes.models import Recipe, RecipeIngredient | ||||
| 
 | ||||
| 
 | ||||
| class RecipeType(DjangoObjectType): | ||||
|     class Meta: | ||||
|         model = Recipe | ||||
| 
 | ||||
| 
 | ||||
| class RecipeIngredientType(DjangoObjectType): | ||||
|     class Meta: | ||||
|         model = RecipeIngredient | ||||
| 
 | ||||
| 
 | ||||
| class Query(graphene.AbstractType): | ||||
|     recipe = graphene.Field(RecipeType, | ||||
|                             id=graphene.Int(), | ||||
|                             title=graphene.String()) | ||||
|     all_recipes = graphene.List(RecipeType) | ||||
| 
 | ||||
|     recipeingredient = graphene.Field(RecipeIngredientType, | ||||
|                                       id=graphene.Int()) | ||||
|     all_recipeingredients = graphene.List(RecipeIngredientType) | ||||
| 
 | ||||
|     def resolve_recipe(self, args, context, info): | ||||
|         id = args.get('id') | ||||
|         title = args.get('title') | ||||
| 
 | ||||
|         if id is not None: | ||||
|             return Recipe.objects.get(pk=id) | ||||
| 
 | ||||
|         if title is not None: | ||||
|             return Recipe.objects.get(title=title) | ||||
| 
 | ||||
|         return None | ||||
| 
 | ||||
|     def resolve_recipeingredient(self, args, context, info): | ||||
|         id = args.get('id') | ||||
| 
 | ||||
|         if id is not None: | ||||
|             return RecipeIngredient.objects.get(pk=id) | ||||
| 
 | ||||
|         return None | ||||
| 
 | ||||
|     def resolve_all_recipes(self, args, context, info): | ||||
|         return Recipe.objects.all() | ||||
| 
 | ||||
|     def resolve_all_recipeingredients(self, args, context, info): | ||||
|         related = ['recipe', 'ingredient'] | ||||
|         return RecipeIngredient.objects.select_related(*related).all() | ||||
							
								
								
									
										14
									
								
								examples/cookbook-plain/cookbook/schema.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								examples/cookbook-plain/cookbook/schema.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| import cookbook.ingredients.schema | ||||
| import cookbook.recipes.schema | ||||
| import graphene | ||||
| 
 | ||||
| from graphene_django.debug import DjangoDebug | ||||
| 
 | ||||
| 
 | ||||
| class Query(cookbook.ingredients.schema.Query, | ||||
|             cookbook.recipes.schema.Query, | ||||
|             graphene.ObjectType): | ||||
|     debug = graphene.Field(DjangoDebug, name='__debug') | ||||
| 
 | ||||
| 
 | ||||
| schema = graphene.Schema(query=Query) | ||||
							
								
								
									
										138
									
								
								examples/cookbook-plain/cookbook/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								examples/cookbook-plain/cookbook/settings.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,138 @@ | |||
| # flake8: noqa | ||||
| """ | ||||
| Django settings for cookbook project. | ||||
| 
 | ||||
| Generated by 'django-admin startproject' using Django 1.9. | ||||
| 
 | ||||
| For more information on this file, see | ||||
| https://docs.djangoproject.com/en/1.9/topics/settings/ | ||||
| 
 | ||||
| For the full list of settings and their values, see | ||||
| https://docs.djangoproject.com/en/1.9/ref/settings/ | ||||
| """ | ||||
| 
 | ||||
| import os | ||||
| 
 | ||||
| # Build paths inside the project like this: os.path.join(BASE_DIR, ...) | ||||
| BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||||
| 
 | ||||
| 
 | ||||
| # Quick-start development settings - unsuitable for production | ||||
| # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ | ||||
| 
 | ||||
| # SECURITY WARNING: keep the secret key used in production secret! | ||||
| SECRET_KEY = '_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4' | ||||
| 
 | ||||
| # SECURITY WARNING: don't run with debug turned on in production! | ||||
| DEBUG = True | ||||
| 
 | ||||
| ALLOWED_HOSTS = [] | ||||
| 
 | ||||
| 
 | ||||
| # Application definition | ||||
| 
 | ||||
| INSTALLED_APPS = [ | ||||
|     'django.contrib.admin', | ||||
|     'django.contrib.auth', | ||||
|     'django.contrib.contenttypes', | ||||
|     'django.contrib.sessions', | ||||
|     'django.contrib.messages', | ||||
|     'django.contrib.staticfiles', | ||||
|     'graphene_django', | ||||
| 
 | ||||
|     'cookbook.ingredients.apps.IngredientsConfig', | ||||
|     'cookbook.recipes.apps.RecipesConfig', | ||||
| ] | ||||
| 
 | ||||
| MIDDLEWARE_CLASSES = [ | ||||
|     'django.middleware.security.SecurityMiddleware', | ||||
|     'django.contrib.sessions.middleware.SessionMiddleware', | ||||
|     'django.middleware.common.CommonMiddleware', | ||||
|     'django.middleware.csrf.CsrfViewMiddleware', | ||||
|     'django.contrib.auth.middleware.AuthenticationMiddleware', | ||||
|     'django.contrib.auth.middleware.SessionAuthenticationMiddleware', | ||||
|     'django.contrib.messages.middleware.MessageMiddleware', | ||||
|     'django.middleware.clickjacking.XFrameOptionsMiddleware', | ||||
| ] | ||||
| 
 | ||||
| GRAPHENE = { | ||||
|     'SCHEMA': 'cookbook.schema.schema', | ||||
|     'MIDDLEWARE': ( | ||||
|         'graphene_django.debug.DjangoDebugMiddleware', | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| ROOT_URLCONF = 'cookbook.urls' | ||||
| 
 | ||||
| TEMPLATES = [ | ||||
|     { | ||||
|         'BACKEND': 'django.template.backends.django.DjangoTemplates', | ||||
|         'DIRS': [], | ||||
|         'APP_DIRS': True, | ||||
|         'OPTIONS': { | ||||
|             'context_processors': [ | ||||
|                 'django.template.context_processors.debug', | ||||
|                 'django.template.context_processors.request', | ||||
|                 'django.contrib.auth.context_processors.auth', | ||||
|                 'django.contrib.messages.context_processors.messages', | ||||
|             ], | ||||
|         }, | ||||
|     }, | ||||
| ] | ||||
| 
 | ||||
| WSGI_APPLICATION = 'cookbook.wsgi.application' | ||||
| 
 | ||||
| 
 | ||||
| # Database | ||||
| # https://docs.djangoproject.com/en/1.9/ref/settings/#databases | ||||
| 
 | ||||
| DATABASES = { | ||||
|     'default': { | ||||
|         'ENGINE': 'django.db.backends.sqlite3', | ||||
|         'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| # Password validation | ||||
| # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators | ||||
| 
 | ||||
| AUTH_PASSWORD_VALIDATORS = [ | ||||
|     { | ||||
|         'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', | ||||
|     }, | ||||
|     { | ||||
|         'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', | ||||
|     }, | ||||
|     { | ||||
|         'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', | ||||
|     }, | ||||
|     { | ||||
|         'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', | ||||
|     }, | ||||
| ] | ||||
| 
 | ||||
| 
 | ||||
| # Internationalization | ||||
| # https://docs.djangoproject.com/en/1.9/topics/i18n/ | ||||
| 
 | ||||
| LANGUAGE_CODE = 'en-us' | ||||
| 
 | ||||
| TIME_ZONE = 'UTC' | ||||
| 
 | ||||
| USE_I18N = True | ||||
| 
 | ||||
| USE_L10N = True | ||||
| 
 | ||||
| USE_TZ = True | ||||
| 
 | ||||
| 
 | ||||
| # Static files (CSS, JavaScript, Images) | ||||
| # https://docs.djangoproject.com/en/1.9/howto/static-files/ | ||||
| 
 | ||||
| STATIC_URL = '/static/' | ||||
| 
 | ||||
| GRAPHENE = { | ||||
|     'SCHEMA': 'cookbook.schema.schema', | ||||
|     'SCHEMA_INDENT': 2, | ||||
| } | ||||
							
								
								
									
										10
									
								
								examples/cookbook-plain/cookbook/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								examples/cookbook-plain/cookbook/urls.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| from django.conf.urls import url | ||||
| from django.contrib import admin | ||||
| 
 | ||||
| from graphene_django.views import GraphQLView | ||||
| 
 | ||||
| 
 | ||||
| urlpatterns = [ | ||||
|     url(r'^admin/', admin.site.urls), | ||||
|     url(r'^graphql', GraphQLView.as_view(graphiql=True)), | ||||
| ] | ||||
							
								
								
									
										16
									
								
								examples/cookbook-plain/cookbook/wsgi.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								examples/cookbook-plain/cookbook/wsgi.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | |||
| """ | ||||
| WSGI config for cookbook project. | ||||
| 
 | ||||
| It exposes the WSGI callable as a module-level variable named ``application``. | ||||
| 
 | ||||
| For more information on this file, see | ||||
| https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ | ||||
| """ | ||||
| 
 | ||||
| import os | ||||
| 
 | ||||
| from django.core.wsgi import get_wsgi_application | ||||
| 
 | ||||
| os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings") | ||||
| 
 | ||||
| application = get_wsgi_application() | ||||
							
								
								
									
										10
									
								
								examples/cookbook-plain/manage.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										10
									
								
								examples/cookbook-plain/manage.py
									
									
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| #!/usr/bin/env python | ||||
| import os | ||||
| import sys | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings") | ||||
| 
 | ||||
|     from django.core.management import execute_from_command_line | ||||
| 
 | ||||
|     execute_from_command_line(sys.argv) | ||||
							
								
								
									
										5
									
								
								examples/cookbook-plain/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								examples/cookbook-plain/requirements.txt
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| graphene | ||||
| graphene-django | ||||
| django_graphiql | ||||
| graphql-core | ||||
| django==1.9 | ||||
							
								
								
									
										2
									
								
								examples/cookbook-plain/setup.cfg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								examples/cookbook-plain/setup.cfg
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | |||
| [flake8] | ||||
| exclude=migrations,.git,__pycache__ | ||||
|  | @ -3,7 +3,7 @@ Cookbook Example Django Project | |||
| 
 | ||||
| This example project demos integration between Graphene and Django. | ||||
| The project contains two apps, one named `ingredients` and another | ||||
| named `recepies`. | ||||
| named `recipes`. | ||||
| 
 | ||||
| Getting started | ||||
| --------------- | ||||
|  |  | |||
|  | @ -27,12 +27,16 @@ def convert_choice_name(name): | |||
| 
 | ||||
| 
 | ||||
| def get_choices(choices): | ||||
|     converted_names = [] | ||||
|     for value, help_text in choices: | ||||
|         if isinstance(help_text, (tuple, list)): | ||||
|             for choice in get_choices(help_text): | ||||
|                 yield choice | ||||
|         else: | ||||
|             name = convert_choice_name(value) | ||||
|             while name in converted_names: | ||||
|                 name += '_' + str(len(converted_names)) | ||||
|             converted_names.append(name) | ||||
|             description = help_text | ||||
|             yield name, value, description | ||||
| 
 | ||||
|  |  | |||
|  | @ -49,6 +49,7 @@ class DjangoConnectionField(ConnectionField): | |||
|             iterable = default_manager | ||||
|         iterable = maybe_queryset(iterable) | ||||
|         if isinstance(iterable, QuerySet): | ||||
|             iterable &= maybe_queryset(default_manager) | ||||
|             _len = iterable.count() | ||||
|         else: | ||||
|             _len = len(iterable) | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ from functools import partial | |||
| 
 | ||||
| from graphene.types.argument import to_arguments | ||||
| from ..fields import DjangoConnectionField | ||||
| from graphene.relay import is_node | ||||
| from .utils import get_filtering_args_from_filterset, get_filterset_class | ||||
| 
 | ||||
| 
 | ||||
|  | @ -28,7 +29,15 @@ class DjangoFilterConnectionField(DjangoConnectionField): | |||
| 
 | ||||
|     @property | ||||
|     def meta(self): | ||||
|         meta = dict(model=self.node_type._meta.model, | ||||
|         if is_node(self.node_type): | ||||
|             _model = self.node_type._meta.model | ||||
|         else: | ||||
|             # ConnectionFields can also be passed Connections, | ||||
|             # in which case, we need to use the Node of the connection | ||||
|             # to get our relevant args. | ||||
|             _model = self.node_type._meta.node._meta.model | ||||
| 
 | ||||
|         meta = dict(model=_model, | ||||
|                     fields=self.fields) | ||||
|         if self._extra_filter_meta: | ||||
|             meta.update(self._extra_filter_meta) | ||||
|  | @ -36,7 +45,16 @@ class DjangoFilterConnectionField(DjangoConnectionField): | |||
| 
 | ||||
|     @property | ||||
|     def fields(self): | ||||
|         return self._fields or self.node_type._meta.filter_fields | ||||
|         if self._fields: | ||||
|             return self._fields | ||||
| 
 | ||||
|         if is_node(self.node_type): | ||||
|             return self.node_type._meta.filter_fields | ||||
|         else: | ||||
|             # ConnectionFields can also be passed Connections, | ||||
|             # in which case, we need to use the Node of the connection | ||||
|             # to get our relevant args. | ||||
|             return self.node_type._meta.node._meta.filter_fields | ||||
| 
 | ||||
|     @property | ||||
|     def args(self): | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ class Registry(object): | |||
|         # assert self.get_type_for_model(cls._meta.model) == cls, ( | ||||
|         #     'Multiple DjangoObjectTypes registered for "{}"'.format(cls._meta.model) | ||||
|         # ) | ||||
|         if not getattr(cls._meta, 'skip_registry', False): | ||||
|             self._registry[cls._meta.model] = cls | ||||
| 
 | ||||
|     def get_type_for_model(self, model): | ||||
|  |  | |||
|  | @ -176,6 +176,22 @@ def test_field_with_choices_gettext(): | |||
|     convert_django_field_with_choices(field) | ||||
| 
 | ||||
| 
 | ||||
| def test_field_with_choices_collision(): | ||||
|     field = models.CharField(help_text='Timezone', choices=( | ||||
|         ('Etc/GMT+1+2', 'Fake choice to produce double collision'), | ||||
|         ('Etc/GMT+1', 'Greenwich Mean Time +1'), | ||||
|         ('Etc/GMT-1', 'Greenwich Mean Time -1'), | ||||
|     )) | ||||
| 
 | ||||
|     class CollisionChoicesModel(models.Model): | ||||
|         timezone = field | ||||
| 
 | ||||
|         class Meta: | ||||
|             app_label = 'test' | ||||
| 
 | ||||
|     convert_django_field_with_choices(field) | ||||
| 
 | ||||
| 
 | ||||
| def test_should_float_convert_float(): | ||||
|     assert_conversion(models.FloatField, graphene.Float) | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ from py.test import raises | |||
| import graphene | ||||
| from graphene.relay import Node | ||||
| 
 | ||||
| from ..utils import DJANGO_FILTER_INSTALLED | ||||
| from ..compat import MissingType, RangeField | ||||
| from ..fields import DjangoConnectionField | ||||
| from ..types import DjangoObjectType | ||||
|  | @ -281,3 +282,85 @@ def test_should_query_connectionfields(): | |||
|             }] | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif(not DJANGO_FILTER_INSTALLED, | ||||
|                     reason="django-filter should be installed") | ||||
| def test_should_query_node_filtering(): | ||||
|     class ReporterType(DjangoObjectType): | ||||
| 
 | ||||
|         class Meta: | ||||
|             model = Reporter | ||||
|             interfaces = (Node, ) | ||||
| 
 | ||||
|     class ArticleType(DjangoObjectType): | ||||
| 
 | ||||
|         class Meta: | ||||
|             model = Article | ||||
|             interfaces = (Node, ) | ||||
|             filter_fields = ('lang', ) | ||||
| 
 | ||||
|     class Query(graphene.ObjectType): | ||||
|         all_reporters = DjangoConnectionField(ReporterType) | ||||
| 
 | ||||
|     r = Reporter.objects.create( | ||||
|         first_name='John', | ||||
|         last_name='Doe', | ||||
|         email='johndoe@example.com', | ||||
|         a_choice=1 | ||||
|     ) | ||||
|     Article.objects.create( | ||||
|         headline='Article Node 1', | ||||
|         pub_date=datetime.date.today(), | ||||
|         reporter=r, | ||||
|         editor=r, | ||||
|         lang='es' | ||||
|     ) | ||||
|     Article.objects.create( | ||||
|         headline='Article Node 2', | ||||
|         pub_date=datetime.date.today(), | ||||
|         reporter=r, | ||||
|         editor=r, | ||||
|         lang='en' | ||||
|     ) | ||||
| 
 | ||||
|     schema = graphene.Schema(query=Query) | ||||
|     query = ''' | ||||
|         query NodeFilteringQuery { | ||||
|             allReporters { | ||||
|                 edges { | ||||
|                     node { | ||||
|                         id | ||||
|                         articles(lang: "es") { | ||||
|                             edges { | ||||
|                                 node { | ||||
|                                     id | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     ''' | ||||
| 
 | ||||
|     expected = { | ||||
|         'allReporters': { | ||||
|             'edges': [{ | ||||
|                 'node': { | ||||
|                     'id': 'UmVwb3J0ZXJUeXBlOjE=', | ||||
|                     'articles': { | ||||
|                         'edges': [{ | ||||
|                             'node': { | ||||
|                                 'id': 'QXJ0aWNsZVR5cGU6MQ==' | ||||
|                             } | ||||
|                         }] | ||||
|                     } | ||||
|                 } | ||||
|             }] | ||||
|         } | ||||
|     } | ||||
|   | ||||
|     result = schema.execute(query) | ||||
|     assert not result.errors | ||||
|     assert result.data == expected | ||||
|  |  | |||
|  | @ -183,6 +183,15 @@ def test_batch_allows_post_with_json_encoding(client): | |||
|     }] | ||||
| 
 | ||||
| 
 | ||||
| def test_batch_fails_if_is_empty(client): | ||||
|     response = client.post(batch_url_string(), '[]', 'application/json') | ||||
| 
 | ||||
|     assert response.status_code == 400 | ||||
|     assert response_json(response) == { | ||||
|         'errors': [{'message': 'Received an empty list in the batch request.'}] | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| def test_allows_sending_a_mutation_via_post(client): | ||||
|     response = client.post(url_string(), j(query='mutation TestMutation { writeTest { test } }'), 'application/json') | ||||
| 
 | ||||
|  | @ -432,9 +441,18 @@ def test_handles_errors_caused_by_a_lack_of_query(client): | |||
|     } | ||||
| 
 | ||||
| 
 | ||||
| def test_handles_invalid_json_bodies(client): | ||||
| def test_handles_not_expected_json_bodies(client): | ||||
|     response = client.post(url_string(), '[]', 'application/json') | ||||
| 
 | ||||
|     assert response.status_code == 400 | ||||
|     assert response_json(response) == { | ||||
|         'errors': [{'message': 'The received data is not a valid JSON query.'}] | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| def test_handles_invalid_json_bodies(client): | ||||
|     response = client.post(url_string(), '[oh}', 'application/json') | ||||
| 
 | ||||
|     assert response.status_code == 400 | ||||
|     assert response_json(response) == { | ||||
|         'errors': [{'message': 'POST body sent invalid JSON.'}] | ||||
|  |  | |||
|  | @ -58,6 +58,7 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): | |||
|             only_fields=(), | ||||
|             exclude_fields=(), | ||||
|             interfaces=(), | ||||
|             skip_registry=False, | ||||
|             registry=None | ||||
|         ) | ||||
|         if DJANGO_FILTER_INSTALLED: | ||||
|  |  | |||
|  | @ -193,10 +193,19 @@ class GraphQLView(View): | |||
|             try: | ||||
|                 request_json = json.loads(request.body.decode('utf-8')) | ||||
|                 if self.batch: | ||||
|                     assert isinstance(request_json, list) | ||||
|                     assert isinstance(request_json, list), ( | ||||
|                         'Batch requests should receive a list, but received {}.' | ||||
|                     ).format(repr(request_json)) | ||||
|                     assert len(request_json) > 0, ( | ||||
|                         'Received an empty list in the batch request.' | ||||
|                     ) | ||||
|                 else: | ||||
|                     assert isinstance(request_json, dict) | ||||
|                     assert isinstance(request_json, dict), ( | ||||
|                         'The received data is not a valid JSON query.' | ||||
|                     ) | ||||
|                 return request_json | ||||
|             except AssertionError as e: | ||||
|                 raise HttpError(HttpResponseBadRequest(str(e))) | ||||
|             except: | ||||
|                 raise HttpError(HttpResponseBadRequest('POST body sent invalid JSON.')) | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										2
									
								
								pytest.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								pytest.ini
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | |||
| [pytest] | ||||
| DJANGO_SETTINGS_MODULE = django_test_settings | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user