• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

mbrochh/django-graphql-apollo-react-demo: Code for a workshop about my Django, G ...

原作者: [db:作者] 来自: 网络 收藏 邀请

开源软件名称(OpenSource Name):

mbrochh/django-graphql-apollo-react-demo

开源软件地址(OpenSource Url):

https://github.com/mbrochh/django-graphql-apollo-react-demo

开源编程语言(OpenSource Language):

Python 49.9%

开源软件介绍(OpenSource Introduction):

A Django + GraphQL + Apollo + React Stack Demo

This repo contains the code shown at the Singapore Djangonauts June 2017 Meetup

A video of the workshop can be found on engineers.sg:

Part 1 Part 2.

The sound in the video is messed up. I'm looking for a venue and an audience to record the talk one more time :(

This README was basically the "slides" for the workshop, so if you want to learn as well, just keep on reading!

In this workshop, we will address the following topics:

Part 1: The Backend

  1. Create a new Django Project
  2. Create a Simple Django App
  3. Add GraphQL to Django
  4. Add Message-DjangoObjectType to GraphQL Schema
  5. Add Mutation to GraphQL Schema
  6. Add JWT-Authentication to Django

Part 2: The Frontend

  1. Create a new React Project
  2. Add ReactRouter to React
  3. Add Apollo to React
  4. Add Query with Variables for DetailView
  5. Add Token Middleware for Authentication
  6. Add Login / Logout Views
  7. Add Mutation for CreateView
  8. Show Form Errors on CreateView
  9. Add Filtering to ListView
  10. Add Pagination to ListView
  11. Add Cache Invalidation

Part 3: Advanced Topics

I am planning to keep this repo alive and add some more best practices as I figure them out at work. Some ideas:

  1. Create a higher order component "LoginRequired" to protect views
  2. Create a higher order component "NetworkStatus" to allow refetching of failed queries after the network was down
  3. Don't refresh the entire page after login/logout
  4. Create Python decorator like "login_required" for mutations and resolvers
  5. Some examples for real cache invalidation
  6. Hosting (EC2 instance for Django, S3 bucket for frontend files)

If you have more ideas, please add them in the issue tracker!

Before you start, you should read a little bit about GraphQL and Apollo and python-graphene.

If you have basic understanding of Python, Django, JavaScript and ReactJS, you should be able to follow this tutorial and copy and paste the code snippets shown below and hopefully it will all work out nicely.

The tutorial should give you a feeling for the necessary steps involved when building a web application with Django, GraphQL and ReactJS.

If you find typos or encounter other issues, please report them at the issue tracker.

Part 1: The Backend

Create a new Django Project

For this demonstration we will need a backend that can serve our GraphQL API. We will chose Django for this, so the first thing we want to do is to create a new Django project. If you are new to Python, you need to read about virtualenvwrapper, first.

mkdir -p ~/Projects/django-graphql-apollo-react-demo/src
cd ~/Projects/django-graphql-apollo-react-demo/src
mkvirtualenv django-graphql-apollo-react-demo
pip install django
pip install pytest
pip install pytest-django
pip install pytest-cov
pip install mixer
django-admin startproject backend
cd backend
./manage.py migrate
./manage.py createsuperuser
./manage.py runserver

You should now be able to browse to localhost:8000/admin/ and login with your superuser account

We like to build our Django apps in a test-driven manner, so let's also create a few files to setup our testing framework:

# File: ./backend/backend/test_settings.py

from .settings import *  # NOQA

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': ':memory:',
    }
}

PASSWORD_HASHERS = (
    'django.contrib.auth.hashers.MD5PasswordHasher',
)

DEFAULT_FILE_STORAGE = 'inmemorystorage.InMemoryStorage'
# File: ./backend/pytest.ini

[pytest]
DJANGO_SETTINGS_MODULE = backend.test_settings
# File: ./backend/.coveragerc

[run]
omit = manage.py, *wsgi.py, *test_settings.py, *settings.py, *urls.py, *__init__.py, */apps.py, */tests/*, */migrations/*

From your ./backend folder, you should now be able to execute pytest --cov-report html --cov . and then open htmlcov/index.html to see the coverage report.

Create a Simple Django App

At this point, our Django project is pretty useless, so let's create a simple Twitter-like app that allows users to create messages. It's a nice example for an app that has a CreateView, a ListView and a DetailView.

cd ~/Projects/django-graphql-apollo-react-demo/src/backend
django-admin startapp simple_app
cd simple_app
mkdir tests
touch tests/__init__.py
touch tests/test_models.py

Whenever we create a new app, we need to tell Django that this app is now part of our project:

# File: ./backend/backend/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'simple_app',
]

First, let's create a test for our upcoming new model. The model doesn't do much, so we will simply test if we are able to create an instance and save it to the DB. We are using mixer to help us with the creation of test-fixtures.

# File: ./backend/simple_app/tests/test_models.py

import pytest
from mixer.backend.django import mixer

# We need to do this so that writing to the DB is possible in our tests.
pytestmark = pytest.mark.django_db


def test_message():
    obj = mixer.blend('simple_app.Message')
    assert obj.pk > 0

Next, let's create our Message model:

# File: ./backend/simple_app/models.py
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models

class Message(models.Model):
    user = models.ForeignKey('auth.User')
    message = models.TextField()
    creation_date = models.DateTimeField(auto_now_add=True)

Let's also register the new model with the Django admin, so that we can add entries to the new table:

# File: ./backend/simple_app/admin.py
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from . import models

admin.site.register(models.Message)

Whenever we make changes to a model, we need to create and run a migration:

cd ~/Projects/django-graphql-apollo-react-demo/src/backend
./manage.py makemigrations simple_app
./manage.py migrate

At this point you should be able to browse to localhost:8000/admin/ and see the table of the new simple_app app.

You should also be able to run pytest and see 1 successful test.

Add GraphQL to Django

Since we have now a Django project with a model, we can start thinking about adding an API. We will use GraphQL for that.

cd ~/Projects/django-graphql-apollo-react-demo/src/backend
pip install graphene-django

Whenever we add a new app to Django, we need to update our INSTALLED_APPS setting. Because of graphene-django, we also need to add one app-specific setting.

# File: ./backend/backend/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'graphene_django',
    'simple_app',
]

GRAPHENE = {
    'SCHEMA': 'backend.schema.schema',
}

Now we need to create our main schema.py file. This file is similar to our main urls.py - it's task is to import all the schema-files in our project and merge them into one big schema.

# File: ./backend/backend/schema.py

import graphene

class Queries(
    graphene.ObjectType
):
    dummy = graphene.String()


schema = graphene.Schema(query=Queries)

Finally, we need to hook up GraphiQL in our Django urls.py:

# File: ./backend/backend/urls.py

from django.conf.urls import url
from django.contrib import admin
from django.views.decorators.csrf import csrf_exempt

from graphene_django.views import GraphQLView


urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^graphiql', csrf_exempt(GraphQLView.as_view(graphiql=True))),
    url(r'^gql', csrf_exempt(GraphQLView.as_view(batch=True))),
]

At this point you should be able to browse to localhost:8000/graphiql and run the query { dummy }

Add Message-DjangoObjectType to GraphQL Schema

If you have used Django Rest Framework before, you know that you have to create serializers for all your models. With GraphQL it is very similar: You have to create Types for all your models.

We will begin with creating a type for our Message model and when we are at it, we will also create a query that returns all messages.

In good TDD fashion, we begin with a test for the type and a test for the query:

# File: ./backend/simple_app/tests/test_schema.py

import pytest
from mixer.backend.django import mixer

from . import schema


pytestmark = pytest.mark.django_db


def test_message_type():
    instance = schema.MessageType()
    assert instance


def test_resolve_all_messages():
    mixer.blend('simple_app.Message')
    mixer.blend('simple_app.Message')
    q = schema.Query()
    res = q.resolve_all_messages(None, None, None)
    assert res.count() == 2, 'Should return all messages'

In order to make our test pass, we will now add our type and the query:

# File: ./backend/simple_app/schema.py

import graphene
from graphene_django.types import DjangoObjectType

from . import models


class MessageType(DjangoObjectType):
    class Meta:
        model = models.Message
        interfaces = (graphene.Node, )


class Query(graphene.AbstractType):
    all_messages = graphene.List(MessageType)

    def resolve_all_messages(self, args, context, info):
        return models.Message.objects.all()

Finally, we need to update your main schema.py file:

# File: ./backend/backend/schema.py

import graphene

import simple_app.schema


class Queries(
    simple_app.schema.Query,
    graphene.ObjectType
):
    dummy = graphene.String()


schema = graphene.Schema(query=Queries)

At this point, you should be able to run pytest and get three passing tests. You should also be able to add a few messages to the DB at localhost:8000/admin/simple_app/message/ You should also be able to browse to localhost:8000/graphiql/ and run the query:

{
  allMessages {
    id, message
  }
}

The query all_messages returns a list of objects. Let's add another query that returns just one object:

# File: ./backend/simple_app/tests/test_schema.py

from graphql_relay.node.node import to_global_id

def test_resolve_message():
    msg = mixer.blend('simple_app.Message')
    q = schema.Query()
    id = to_global_id('MessageType', msg.pk)
    res = q.resolve_messages({'id': id}, None, None)
    assert res == msg, 'Should return the requested message'

To make the test pass, let's update our schema file:

# File: ./backend/simple_app/schema.py

from graphql_relay.node.node import from_global_id

class Query(graphene.AbstractType):
    message = graphene.Field(MessageType, id=graphene.ID())

    def resolve_message(self, args, context, info):
        rid = from_global_id(args.get('id'))
        # rid is a tuple: ('MessageType', '1')
        return models.Message.objects.get(pk=rid[1])

    [...]

At this point you should be able to run pytest and see four passing tests You should also be able to browse to graphiql and run the query { message(id: "TWVzc2FnZVR5cGU6MQ==") { id, message } }

Add Mutation to GraphQL Schema

Our API is able to return items from our DB. Now it is time to allow to write messages. Anything that changes data in GraphQL is called a "Mutation". We want to ensure that our mutation does three things:

  1. Return a 403 status if the user is not logged in
  2. Return a 400 status and form errors if the user does not provide a message
  3. Return a 200 status and the newly created message if everything is OK
# File: ./backend/simple_app/tests/test_schema.py

from django.contrib.auth.models import AnonymousUser
from django.test import RequestFactory

def test_create_message_mutation():
    user = mixer.blend('auth.User')
    mut = schema.CreateMessageMutation()

    data = {'message': 'Test'}
    req = RequestFactory().get('/')
    req.user = AnonymousUser()
    res = mut.mutate(None, data, req, None)
    assert res.status == 403, 'Should return 403 if user is not logged in'

    req.user = user
    res = mut.mutate(None, {}, req, None)
    assert res.status == 400, 'Should return 400 if there are form errors'
    assert 'message' in res.formErrors, (
        'Should have form error for message field')

    res = mut.mutate(None, data, req, None)
    assert res.status == 200, 'Should return 200 if mutation is successful'
    assert res.message.pk == 1, 'Should create new message'

With these tests in place, we can implement the actual mutation:

# File: ./backend/simple_app/schema.py

import json

class CreateMessageMutation(graphene.Mutation):
    class Input:
        message = graphene.String()

    status = graphene.Int()
    formErrors = graphene.String()
    message = graphene.Field(MessageType)

    @staticmethod
    def mutate(root, args, context, info):
        if not context.user.is_authenticated():
            return CreateMessageMutation(status=403)
        message = args.get('message', '').strip()
        # Here we would usually use Django forms to validate the input
        if not message:
            return CreateMessageMutation(
                status=400,
                formErrors=json.dumps(
                    {'message': ['Please enter a message.']}))
        obj = models.Message.objects.create(
            user=context.user, message=message
        )
        return CreateMessageMutation(status=200, message=obj)


class Mutation(graphene.AbstractType):
    create_message = CreateMessageMutation.Field()

This new Mutation class is currently not hooked up in our main schema.py file, so let's add that:

# File: ./backend/backend/schema.py

class Mutations(
    simple_app.schema.Mutation,
    graphene.ObjectType,
):
    pass

[...]

schema = graphene.Schema(query=Queries, mutation=Mutations)

At this point you should be able to run pytest and get five passing tests. You should also be able to browse to graphiql and run this mutation:

mutation {
  createMessage(message: "Test") {
    status,
    formErrors,
    message {
      id
    }
  }
}

Add JWT-Authentication to Django

One of the most common things that every web application needs is authentication. During my research I found django-graph-auth, which is based on Django Rest Frameworks JWT plugin. There is als pyjwt, which would allow you to implement your own endpoints.

I didn't have the time to evaluate django-graph-auth yet, and I didn't have the confidence to run my own implementation. For that reason, I chose the practical approach and used what is tried and tested by a very large user base and added Django Rest Framework and django-rest-framework-jwt to the project.

We will also need to install django-cors-headers because during local development, the backend and the frontend are served from different ports, so we need to enable to accept requests from all origins.

cd ~/Projects/django-graphql-apollo-react-demo/src/backend
pip install djangorestframework
pip install djangorestframework-jwt
pip install django-cors-headers

Now we need to update our Django settings with new settings related to the rest_framework and corsheaders apps:

# File: ./backend/backend/settings.py**

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'corsheaders',
    'graphene_django',
    'simple_app',
]

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
}

CORS_ORIGIN_ALLOW_ALL = True

MIDDLEWARE_CLASSES = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    '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',
]

Now that Rest Framework is configured, we need to add a few URLs:

# File: ./backend/backend/urls.py

[...]
from rest_framework_jwt.views import obtain_jwt_token
from rest_framework_jwt.views import refresh_jwt_token
from rest_framework_jwt.views import verify_jwt_token


urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^graphiql', csrf_exempt(GraphQLView.as_view(graphiql=True))),
    url(r'^gql', csrf_exempt(GraphQLView.as_view(batch=True))),
    url(r'^api-token-auth/', obtain_jwt_token),
    url(r'^api-token-refresh/', refresh_jwt_token),
    url(r'^api-token-verify/', verify_jwt_token),
]

At this point you should be able to get a token by sending this request: curl -X POST -d "username=admin&password=test1234" http://localhost:8000/api-token-auth/

Part 2: The Frontend

In Part 1, we create a Django backend that serves a GraphQL API. In this part we will create a ReactJS frontend that consumes that API.

Create a new React Project

Facebook has released a wonderful command line tool that kickstarts a new ReactJS project with a powerful webpack configuration. Let's use that:

cd ~/Projects/django-graphql-apollo-react-demo/src
npm install -g create-react-app
create-react-app frontend
cd frontend
yarn start

At this point you should be able to run yarn start and the new ReactJS project should open up in a browser tab

Add ReactRouter to React

cd ~/Projects/django-graphql-apollo-react-demo/src/frontend
yarn add react-router-dom

First, we need to replace the example code in App.js with our own code:


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap