From 61dd8c4277d3962fa980bd55248f35d6289c1520 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Sun, 12 Feb 2017 16:44:39 +0200 Subject: [PATCH 01/10] Add basic structure for plain tutorial - `tutorial.rst` is now `tutorial-relay.rst` - Added `tutorial-plain.rst` --- docs/index.rst | 3 +- docs/tutorial-plain.rst | 504 ++++++++++++++++++++++ docs/{tutorial.rst => tutorial-relay.rst} | 4 +- 3 files changed, 508 insertions(+), 3 deletions(-) create mode 100644 docs/tutorial-plain.rst rename docs/{tutorial.rst => tutorial-relay.rst} (99%) diff --git a/docs/index.rst b/docs/index.rst index c8b5515..ccc6bd4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,8 @@ Contents: .. toctree:: :maxdepth: 0 - tutorial + tutorial-plain + tutorial-relay filtering authorization debug diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst new file mode 100644 index 0000000..6653b52 --- /dev/null +++ b/docs/tutorial-plain.rst @@ -0,0 +1,504 @@ +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 `__ documentation first. + +Setup the Django project +------------------------ + +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 `__ +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 params 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/graphiql `__ 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 `__ that is happeing. + + +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 excercise, 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 `__ diff --git a/docs/tutorial.rst b/docs/tutorial-relay.rst similarity index 99% rename from docs/tutorial.rst rename to docs/tutorial-relay.rst index 56c492d..d544bac 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial-relay.rst @@ -1,5 +1,5 @@ -Graphene-Django Tutorial -======================== +Graphene-Django Tutorial using Relay +==================================== Graphene has a number of additional features that are designed to make working with Django *really simple*. From 1304183893f3c31d09cabcc5cbd845bf1189cbaf Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Sun, 12 Feb 2017 16:48:48 +0200 Subject: [PATCH 02/10] Spellcheck `tutorial-plain.rst` --- docs/tutorial-plain.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index 6653b52..2a76a25 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -218,7 +218,7 @@ 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 params with ``graphiql=True``. +aforementioned GraphiQL we specify that on the parameters with ``graphiql=True``. .. code:: python @@ -308,7 +308,7 @@ If you are using the provided fixtures, you will see the following response: You can experiment with ``allCategories`` too. -Something to have in mind is the `auto camelcasing `__ that is happeing. +Something to have in mind is the `auto camelcasing `__ that is happening. Getting relations From 950cab641b9addbcf722226b7bcb3b36b10e85ab Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Sun, 12 Feb 2017 16:48:59 +0200 Subject: [PATCH 03/10] Bump year to 2017 --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index d729246..2ea2d55 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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 From b55e988205b6f14c79c177024b4a9bcca7c1d1aa Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Tue, 14 Feb 2017 19:55:38 +0200 Subject: [PATCH 04/10] Add `Summary` section with some paragraphs --- docs/tutorial-plain.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index 2a76a25..f1c16ee 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -499,6 +499,14 @@ This will give us the following results: } } -As an excercise, you can try making some queries to ``ingredient``. +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 `__ + + +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``. + +In the next tutorial, we are going to use the **Relay specificiation** combined with ``django-filter`` From 4fc3dd66a197cd5df1d430cf34aacc31fc63e4c0 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Tue, 14 Feb 2017 20:22:51 +0200 Subject: [PATCH 05/10] Add `Vim.gitignore` to `.gitignore` - Taken from --- .gitignore | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.gitignore b/.gitignore index 2c4ca2b..0b25625 100644 --- a/.gitignore +++ b/.gitignore @@ -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 From 531a5348e6c88ea313a97649d35375b1923d9d54 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Tue, 14 Feb 2017 20:23:45 +0200 Subject: [PATCH 06/10] Add `examples/cookbook-plain` to follow the plain tutorial --- examples/cookbook-plain/README.md | 64 ++++++++ examples/cookbook-plain/cookbook/__init__.py | 0 .../cookbook/ingredients/__init__.py | 0 .../cookbook/ingredients/admin.py | 12 ++ .../cookbook/ingredients/apps.py | 7 + .../ingredients/fixtures/ingredients.json | 1 + .../ingredients/migrations/0001_initial.py | 33 +++++ .../migrations/0002_auto_20161104_0050.py | 20 +++ .../ingredients/migrations/__init__.py | 0 .../cookbook/ingredients/models.py | 17 +++ .../cookbook/ingredients/schema.py | 57 ++++++++ .../cookbook/recipes/__init__.py | 0 .../cookbook-plain/cookbook/recipes/admin.py | 12 ++ .../cookbook-plain/cookbook/recipes/apps.py | 7 + .../recipes/migrations/0001_initial.py | 36 +++++ .../migrations/0002_auto_20161104_0106.py | 25 ++++ .../cookbook/recipes/migrations/__init__.py | 0 .../cookbook-plain/cookbook/recipes/models.py | 20 +++ .../cookbook-plain/cookbook/recipes/schema.py | 52 +++++++ examples/cookbook-plain/cookbook/schema.py | 14 ++ examples/cookbook-plain/cookbook/settings.py | 138 ++++++++++++++++++ examples/cookbook-plain/cookbook/urls.py | 10 ++ examples/cookbook-plain/cookbook/wsgi.py | 16 ++ examples/cookbook-plain/manage.py | 10 ++ examples/cookbook-plain/requirements.txt | 5 + examples/cookbook-plain/setup.cfg | 2 + 26 files changed, 558 insertions(+) create mode 100644 examples/cookbook-plain/README.md create mode 100644 examples/cookbook-plain/cookbook/__init__.py create mode 100644 examples/cookbook-plain/cookbook/ingredients/__init__.py create mode 100644 examples/cookbook-plain/cookbook/ingredients/admin.py create mode 100644 examples/cookbook-plain/cookbook/ingredients/apps.py create mode 100644 examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json create mode 100644 examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py create mode 100644 examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py create mode 100644 examples/cookbook-plain/cookbook/ingredients/migrations/__init__.py create mode 100644 examples/cookbook-plain/cookbook/ingredients/models.py create mode 100644 examples/cookbook-plain/cookbook/ingredients/schema.py create mode 100644 examples/cookbook-plain/cookbook/recipes/__init__.py create mode 100644 examples/cookbook-plain/cookbook/recipes/admin.py create mode 100644 examples/cookbook-plain/cookbook/recipes/apps.py create mode 100644 examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py create mode 100644 examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py create mode 100644 examples/cookbook-plain/cookbook/recipes/migrations/__init__.py create mode 100644 examples/cookbook-plain/cookbook/recipes/models.py create mode 100644 examples/cookbook-plain/cookbook/recipes/schema.py create mode 100644 examples/cookbook-plain/cookbook/schema.py create mode 100644 examples/cookbook-plain/cookbook/settings.py create mode 100644 examples/cookbook-plain/cookbook/urls.py create mode 100644 examples/cookbook-plain/cookbook/wsgi.py create mode 100755 examples/cookbook-plain/manage.py create mode 100644 examples/cookbook-plain/requirements.txt create mode 100644 examples/cookbook-plain/setup.cfg diff --git a/examples/cookbook-plain/README.md b/examples/cookbook-plain/README.md new file mode 100644 index 0000000..018c584 --- /dev/null +++ b/examples/cookbook-plain/README.md @@ -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) diff --git a/examples/cookbook-plain/cookbook/__init__.py b/examples/cookbook-plain/cookbook/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/cookbook-plain/cookbook/ingredients/__init__.py b/examples/cookbook-plain/cookbook/ingredients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/cookbook-plain/cookbook/ingredients/admin.py b/examples/cookbook-plain/cookbook/ingredients/admin.py new file mode 100644 index 0000000..b57cbc3 --- /dev/null +++ b/examples/cookbook-plain/cookbook/ingredients/admin.py @@ -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) diff --git a/examples/cookbook-plain/cookbook/ingredients/apps.py b/examples/cookbook-plain/cookbook/ingredients/apps.py new file mode 100644 index 0000000..21b4b08 --- /dev/null +++ b/examples/cookbook-plain/cookbook/ingredients/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class IngredientsConfig(AppConfig): + name = 'cookbook.ingredients' + label = 'ingredients' + verbose_name = 'Ingredients' diff --git a/examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json b/examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json new file mode 100644 index 0000000..8625d3c --- /dev/null +++ b/examples/cookbook-plain/cookbook/ingredients/fixtures/ingredients.json @@ -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}}] \ No newline at end of file diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py new file mode 100644 index 0000000..0494923 --- /dev/null +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py new file mode 100644 index 0000000..359d4fc --- /dev/null +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py @@ -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), + ), + ] diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/__init__.py b/examples/cookbook-plain/cookbook/ingredients/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/cookbook-plain/cookbook/ingredients/models.py b/examples/cookbook-plain/cookbook/ingredients/models.py new file mode 100644 index 0000000..2f0eba3 --- /dev/null +++ b/examples/cookbook-plain/cookbook/ingredients/models.py @@ -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 diff --git a/examples/cookbook-plain/cookbook/ingredients/schema.py b/examples/cookbook-plain/cookbook/ingredients/schema.py new file mode 100644 index 0000000..895f216 --- /dev/null +++ b/examples/cookbook-plain/cookbook/ingredients/schema.py @@ -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 diff --git a/examples/cookbook-plain/cookbook/recipes/__init__.py b/examples/cookbook-plain/cookbook/recipes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/cookbook-plain/cookbook/recipes/admin.py b/examples/cookbook-plain/cookbook/recipes/admin.py new file mode 100644 index 0000000..10d568f --- /dev/null +++ b/examples/cookbook-plain/cookbook/recipes/admin.py @@ -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] diff --git a/examples/cookbook-plain/cookbook/recipes/apps.py b/examples/cookbook-plain/cookbook/recipes/apps.py new file mode 100644 index 0000000..1f24f13 --- /dev/null +++ b/examples/cookbook-plain/cookbook/recipes/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class RecipesConfig(AppConfig): + name = 'cookbook.recipes' + label = 'recipes' + verbose_name = 'Recipes' diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py b/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py new file mode 100644 index 0000000..338c71a --- /dev/null +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py b/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py new file mode 100644 index 0000000..f135392 --- /dev/null +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py @@ -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), + ), + ] diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/__init__.py b/examples/cookbook-plain/cookbook/recipes/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/cookbook-plain/cookbook/recipes/models.py b/examples/cookbook-plain/cookbook/recipes/models.py new file mode 100644 index 0000000..e688044 --- /dev/null +++ b/examples/cookbook-plain/cookbook/recipes/models.py @@ -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'), + )) diff --git a/examples/cookbook-plain/cookbook/recipes/schema.py b/examples/cookbook-plain/cookbook/recipes/schema.py new file mode 100644 index 0000000..8ea1ccd --- /dev/null +++ b/examples/cookbook-plain/cookbook/recipes/schema.py @@ -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() diff --git a/examples/cookbook-plain/cookbook/schema.py b/examples/cookbook-plain/cookbook/schema.py new file mode 100644 index 0000000..f8606a7 --- /dev/null +++ b/examples/cookbook-plain/cookbook/schema.py @@ -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) diff --git a/examples/cookbook-plain/cookbook/settings.py b/examples/cookbook-plain/cookbook/settings.py new file mode 100644 index 0000000..948292d --- /dev/null +++ b/examples/cookbook-plain/cookbook/settings.py @@ -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, +} diff --git a/examples/cookbook-plain/cookbook/urls.py b/examples/cookbook-plain/cookbook/urls.py new file mode 100644 index 0000000..9f8755b --- /dev/null +++ b/examples/cookbook-plain/cookbook/urls.py @@ -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)), +] diff --git a/examples/cookbook-plain/cookbook/wsgi.py b/examples/cookbook-plain/cookbook/wsgi.py new file mode 100644 index 0000000..954b0a8 --- /dev/null +++ b/examples/cookbook-plain/cookbook/wsgi.py @@ -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() diff --git a/examples/cookbook-plain/manage.py b/examples/cookbook-plain/manage.py new file mode 100755 index 0000000..8d8a34d --- /dev/null +++ b/examples/cookbook-plain/manage.py @@ -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) diff --git a/examples/cookbook-plain/requirements.txt b/examples/cookbook-plain/requirements.txt new file mode 100644 index 0000000..6931cf4 --- /dev/null +++ b/examples/cookbook-plain/requirements.txt @@ -0,0 +1,5 @@ +graphene +graphene-django +django_graphiql +graphql-core +django==1.9 diff --git a/examples/cookbook-plain/setup.cfg b/examples/cookbook-plain/setup.cfg new file mode 100644 index 0000000..8c6a6e8 --- /dev/null +++ b/examples/cookbook-plain/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +exclude=migrations,.git,__pycache__ From 9621054852fe7449a5d64f2c1624e1c5ea6ccad1 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Tue, 14 Feb 2017 20:28:49 +0200 Subject: [PATCH 07/10] Add reference to `cookbook-plain` in `tutorial-plain.rst` --- docs/tutorial-plain.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index f1c16ee..ac11430 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -11,6 +11,10 @@ A good idea is to check the `graphene Date: Tue, 21 Feb 2017 21:15:43 +0200 Subject: [PATCH 08/10] Improve summary section - Mention automatic pagination --- docs/tutorial-plain.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index ac11430..b91f886 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -513,4 +513,4 @@ 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``. -In the next tutorial, we are going to use the **Relay specificiation** combined with ``django-filter`` +If you want to put things like ``django-filter`` and automatic pagination in action, you should continue with the **relay tutorial.** From f15d2f67621f4857a8b37aef133087d1c17fe8d1 Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Tue, 21 Feb 2017 21:32:45 +0200 Subject: [PATCH 09/10] Update `tutorial-relay.rst` structure to match plain --- docs/tutorial-relay.rst | 77 ++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/docs/tutorial-relay.rst b/docs/tutorial-relay.rst index d544bac..45d2207 100644 --- a/docs/tutorial-relay.rst +++ b/docs/tutorial-relay.rst @@ -1,5 +1,5 @@ -Graphene-Django Tutorial using Relay -==================================== +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 `__. +A good idea is to check the following things first: + +* `Graphene Relay documentation `__ +* `GraphQL Relay Specification `__ + 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 `__ +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 ------ @@ -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 `__ -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. From 5d7794815f3ee92528b1b7657bacbb246bcc5dda Mon Sep 17 00:00:00 2001 From: Radoslav Georgiev Date: Tue, 21 Feb 2017 21:39:25 +0200 Subject: [PATCH 10/10] Fix GraphiQL URL which is configured in the tutorial. - Based on --- docs/tutorial-plain.rst | 2 +- docs/tutorial-relay.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index b91f886..a4c98a9 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -271,7 +271,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 `__ and +Go to `localhost:8000/graphql `__ and type your first query! .. code:: diff --git a/docs/tutorial-relay.rst b/docs/tutorial-relay.rst index 45d2207..895dc44 100644 --- a/docs/tutorial-relay.rst +++ b/docs/tutorial-relay.rst @@ -281,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 `__ and +Go to `localhost:8000/graphql `__ and type your first query! .. code::