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 | # Databases | ||||||
| *.sqlite3 | *.sqlite3 | ||||||
| .vscode | .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 | 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 | working with Graphene and Django: limiting which fields are accessible | ||||||
| via GraphQL and limiting which objects a user can access. | 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') |             only_fields = ('title', 'content') | ||||||
|             interfaces = (relay.Node, ) |             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 | Queryset Filtering On Lists | ||||||
| --------------------------- | --------------------------- | ||||||
| 
 | 
 | ||||||
|  | @ -108,3 +122,28 @@ method to your ``DjangoObjectType``. | ||||||
|             if post.published or context.user == post.owner: |             if post.published or context.user == post.owner: | ||||||
|                 return post |                 return post | ||||||
|             return None |             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. | # General information about the project. | ||||||
| project = u'Graphene Django' | project = u'Graphene Django' | ||||||
| copyright = u'Graphene 2016' | copyright = u'Graphene 2017' | ||||||
| author = u'Syrus Akbary' | author = u'Syrus Akbary' | ||||||
| 
 | 
 | ||||||
| # The version info for the project you're documenting, acts as replacement for | # The version info for the project you're documenting, acts as replacement for | ||||||
|  |  | ||||||
|  | @ -6,7 +6,8 @@ Contents: | ||||||
| .. toctree:: | .. toctree:: | ||||||
|    :maxdepth: 0 |    :maxdepth: 0 | ||||||
| 
 | 
 | ||||||
|    tutorial |    tutorial-plain | ||||||
|  |    tutorial-relay | ||||||
|    filtering |    filtering | ||||||
|    authorization |    authorization | ||||||
|    debug |    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 | Graphene has a number of additional features that are designed to make | ||||||
| working with Django *really simple*. | 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 | Note: The code in this quickstart is pulled from the `cookbook example | ||||||
| app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__. | 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 | Setup the Django project | ||||||
| ------------------------ | ------------------------ | ||||||
| 
 | 
 | ||||||
|  | @ -43,7 +48,7 @@ Now sync your database for the first time: | ||||||
| Let's create a few simple models... | Let's create a few simple models... | ||||||
| 
 | 
 | ||||||
| Defining our models | Defining our models | ||||||
| ------------------- | ^^^^^^^^^^^^^^^^^^^ | ||||||
| 
 | 
 | ||||||
| Let's get started with these models: | Let's get started with these models: | ||||||
| 
 | 
 | ||||||
|  | @ -68,6 +73,33 @@ Let's get started with these models: | ||||||
|         def __str__(self): |         def __str__(self): | ||||||
|             return self.name |             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 | Schema | ||||||
| ------ | ------ | ||||||
| 
 | 
 | ||||||
|  | @ -90,7 +122,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following: | ||||||
|     from graphene_django import DjangoObjectType |     from graphene_django import DjangoObjectType | ||||||
|     from graphene_django.filter import DjangoFilterConnectionField |     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. |     # 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 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 |         # This class will inherit from multiple Queries | ||||||
|         # as we begin to add more apps to our project |         # as we begin to add more apps to our project | ||||||
|         pass |         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`` | You can think of this as being something like your top-level ``urls.py`` | ||||||
| file (although it currently lacks any namespacing). | file (although it currently lacks any namespacing). | ||||||
| 
 | 
 | ||||||
|  | Testing everything so far | ||||||
|  | ------------------------- | ||||||
|  | 
 | ||||||
| Update settings | Update settings | ||||||
| --------------- | ^^^^^^^^^^^^^^^ | ||||||
| 
 | 
 | ||||||
| Next, install your app and GraphiQL in your Django project. GraphiQL is | Next, install your app and GraphiQL in your Django project. GraphiQL is | ||||||
| a web-based integrated development environment to assist in the writing | 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. | as explained below. | ||||||
| 
 | 
 | ||||||
| Creating GraphQL and GraphiQL views | Creating GraphQL and GraphiQL views | ||||||
| ----------------------------------- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
| 
 | 
 | ||||||
| Unlike a RESTful API, there is only a single URL from which GraphQL is | 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`` | 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)), |         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 | Testing our GraphQL schema | ||||||
| -------------------------- | ^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
| 
 | 
 | ||||||
| We're now ready to test the API we've built. Let's fire up the server | We're now ready to test the API we've built. Let's fire up the server | ||||||
| from the command line. | from the command line. | ||||||
|  | @ -276,7 +281,7 @@ from the command line. | ||||||
|     Starting development server at http://127.0.0.1:8000/ |     Starting development server at http://127.0.0.1:8000/ | ||||||
|     Quit the server with CONTROL-C. |     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! | type your first query! | ||||||
| 
 | 
 | ||||||
| .. code:: | .. 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. | This example project demos integration between Graphene and Django. | ||||||
| The project contains two apps, one named `ingredients` and another | The project contains two apps, one named `ingredients` and another | ||||||
| named `recepies`. | named `recipes`. | ||||||
| 
 | 
 | ||||||
| Getting started | Getting started | ||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
|  | @ -27,12 +27,16 @@ def convert_choice_name(name): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_choices(choices): | def get_choices(choices): | ||||||
|  |     converted_names = [] | ||||||
|     for value, help_text in choices: |     for value, help_text in choices: | ||||||
|         if isinstance(help_text, (tuple, list)): |         if isinstance(help_text, (tuple, list)): | ||||||
|             for choice in get_choices(help_text): |             for choice in get_choices(help_text): | ||||||
|                 yield choice |                 yield choice | ||||||
|         else: |         else: | ||||||
|             name = convert_choice_name(value) |             name = convert_choice_name(value) | ||||||
|  |             while name in converted_names: | ||||||
|  |                 name += '_' + str(len(converted_names)) | ||||||
|  |             converted_names.append(name) | ||||||
|             description = help_text |             description = help_text | ||||||
|             yield name, value, description |             yield name, value, description | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -49,6 +49,7 @@ class DjangoConnectionField(ConnectionField): | ||||||
|             iterable = default_manager |             iterable = default_manager | ||||||
|         iterable = maybe_queryset(iterable) |         iterable = maybe_queryset(iterable) | ||||||
|         if isinstance(iterable, QuerySet): |         if isinstance(iterable, QuerySet): | ||||||
|  |             iterable &= maybe_queryset(default_manager) | ||||||
|             _len = iterable.count() |             _len = iterable.count() | ||||||
|         else: |         else: | ||||||
|             _len = len(iterable) |             _len = len(iterable) | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ from functools import partial | ||||||
| 
 | 
 | ||||||
| from graphene.types.argument import to_arguments | from graphene.types.argument import to_arguments | ||||||
| from ..fields import DjangoConnectionField | from ..fields import DjangoConnectionField | ||||||
|  | from graphene.relay import is_node | ||||||
| from .utils import get_filtering_args_from_filterset, get_filterset_class | from .utils import get_filtering_args_from_filterset, get_filterset_class | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -28,7 +29,15 @@ class DjangoFilterConnectionField(DjangoConnectionField): | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def meta(self): |     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) |                     fields=self.fields) | ||||||
|         if self._extra_filter_meta: |         if self._extra_filter_meta: | ||||||
|             meta.update(self._extra_filter_meta) |             meta.update(self._extra_filter_meta) | ||||||
|  | @ -36,7 +45,16 @@ class DjangoFilterConnectionField(DjangoConnectionField): | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def fields(self): |     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 |     @property | ||||||
|     def args(self): |     def args(self): | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ class Registry(object): | ||||||
|         # assert self.get_type_for_model(cls._meta.model) == cls, ( |         # assert self.get_type_for_model(cls._meta.model) == cls, ( | ||||||
|         #     'Multiple DjangoObjectTypes registered for "{}"'.format(cls._meta.model) |         #     'Multiple DjangoObjectTypes registered for "{}"'.format(cls._meta.model) | ||||||
|         # ) |         # ) | ||||||
|  |         if not getattr(cls._meta, 'skip_registry', False): | ||||||
|             self._registry[cls._meta.model] = cls |             self._registry[cls._meta.model] = cls | ||||||
| 
 | 
 | ||||||
|     def get_type_for_model(self, model): |     def get_type_for_model(self, model): | ||||||
|  |  | ||||||
|  | @ -176,6 +176,22 @@ def test_field_with_choices_gettext(): | ||||||
|     convert_django_field_with_choices(field) |     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(): | def test_should_float_convert_float(): | ||||||
|     assert_conversion(models.FloatField, graphene.Float) |     assert_conversion(models.FloatField, graphene.Float) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ from py.test import raises | ||||||
| import graphene | import graphene | ||||||
| from graphene.relay import Node | from graphene.relay import Node | ||||||
| 
 | 
 | ||||||
|  | from ..utils import DJANGO_FILTER_INSTALLED | ||||||
| from ..compat import MissingType, RangeField | from ..compat import MissingType, RangeField | ||||||
| from ..fields import DjangoConnectionField | from ..fields import DjangoConnectionField | ||||||
| from ..types import DjangoObjectType | 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): | def test_allows_sending_a_mutation_via_post(client): | ||||||
|     response = client.post(url_string(), j(query='mutation TestMutation { writeTest { test } }'), 'application/json') |     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') |     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.status_code == 400 | ||||||
|     assert response_json(response) == { |     assert response_json(response) == { | ||||||
|         'errors': [{'message': 'POST body sent invalid JSON.'}] |         'errors': [{'message': 'POST body sent invalid JSON.'}] | ||||||
|  |  | ||||||
|  | @ -58,6 +58,7 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): | ||||||
|             only_fields=(), |             only_fields=(), | ||||||
|             exclude_fields=(), |             exclude_fields=(), | ||||||
|             interfaces=(), |             interfaces=(), | ||||||
|  |             skip_registry=False, | ||||||
|             registry=None |             registry=None | ||||||
|         ) |         ) | ||||||
|         if DJANGO_FILTER_INSTALLED: |         if DJANGO_FILTER_INSTALLED: | ||||||
|  |  | ||||||
|  | @ -193,10 +193,19 @@ class GraphQLView(View): | ||||||
|             try: |             try: | ||||||
|                 request_json = json.loads(request.body.decode('utf-8')) |                 request_json = json.loads(request.body.decode('utf-8')) | ||||||
|                 if self.batch: |                 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: |                 else: | ||||||
|                     assert isinstance(request_json, dict) |                     assert isinstance(request_json, dict), ( | ||||||
|  |                         'The received data is not a valid JSON query.' | ||||||
|  |                     ) | ||||||
|                 return request_json |                 return request_json | ||||||
|  |             except AssertionError as e: | ||||||
|  |                 raise HttpError(HttpResponseBadRequest(str(e))) | ||||||
|             except: |             except: | ||||||
|                 raise HttpError(HttpResponseBadRequest('POST body sent invalid JSON.')) |                 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 | ||||||
|  | @ -1,9 +1,6 @@ | ||||||
| [aliases] | [aliases] | ||||||
| test=pytest | test=pytest | ||||||
| 
 | 
 | ||||||
| [tool:pytest] |  | ||||||
| DJANGO_SETTINGS_MODULE = django_test_settings |  | ||||||
| 
 |  | ||||||
| [flake8] | [flake8] | ||||||
| exclude = setup.py,docs/*,examples/*,tests,graphene_django/debug/sql/* | exclude = setup.py,docs/*,examples/*,tests,graphene_django/debug/sql/* | ||||||
| max-line-length = 120 | max-line-length = 120 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user