diff --git a/.travis.yml b/.travis.yml index 3531b56..bbeeb80 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,8 +12,17 @@ after_success: - pip install coveralls - coveralls -matrix: +stages: + - test + - name: deploy + if: tag IS present + +jobs: fast_finish: true + + allow_failures: + - env: DJANGO=master + include: - python: 2.7 env: DJANGO=1.11 @@ -56,14 +65,15 @@ matrix: - python: 3.7 env: TOXENV=black,flake8 - allow_failures: - - env: DJANGO=master - -deploy: - provider: pypi - user: syrusakbary - on: - tags: true - password: - secure: kymIFCEPUbkgRqe2NAXkWfxMmGRfWvWBOP6LIXdVdkOOkm91fU7bndPGrAjos+/7gN0Org609ZmHSlVXNMJUWcsL2or/x5LcADJ4cZDe+79qynuoRb9xs1Ri4O4SBAuVMZxuVJvs8oUzT2R11ql5vASSMtXgbX+ZDGpmPRVZStkCuXgOc4LBhbPKyl3OFy7UQFPgAEmy3Yjh4ZSKzlXheK+S6mmr60+DCIjpaA0BWPxYK9FUE0qm7JJbHLUbwsUP/QMp5MmGjwFisXCNsIe686B7QKRaiOw62eJc2R7He8AuEC8T9OM4kRwDlecSn8mMpkoSB7QWtlJ+6XdLrJFPNvtrOfgfzS9/96Qrw9WlOslk68hMlhJeRb0s2YUD8tiV3UUkvbL1mfFoS4SI9U+rojS55KhUEJWHg1w7DjoOPoZmaIL2ChRupmvrFYNAGae1cxwG3Urh+t3wYlN3gpKsRDe5GOT7Wm2tr0ad3McCpDGUwSChX59BAJXe/MoLxkKScTrMyR8yMxHOF0b4zpVn5l7xB/o2Ik4zavx5q/0rGBMK2D+5d+gpQogKShoquTPsZUwO7sB5hYeH2hqGqpeGzZtb76E2zZYd18pJ0FsBudm5+KWjYdZ+vbtGrLxdTXJ1EEtzVXm0lscykTpqUucbXSa51dhStJvW2xEEz6p3rHo= - distributions: "sdist bdist_wheel" + - stage: deploy + script: skip + python: 3.7 + after_success: true + deploy: + provider: pypi + user: syrusakbary + on: + tags: true + password: + secure: kymIFCEPUbkgRqe2NAXkWfxMmGRfWvWBOP6LIXdVdkOOkm91fU7bndPGrAjos+/7gN0Org609ZmHSlVXNMJUWcsL2or/x5LcADJ4cZDe+79qynuoRb9xs1Ri4O4SBAuVMZxuVJvs8oUzT2R11ql5vASSMtXgbX+ZDGpmPRVZStkCuXgOc4LBhbPKyl3OFy7UQFPgAEmy3Yjh4ZSKzlXheK+S6mmr60+DCIjpaA0BWPxYK9FUE0qm7JJbHLUbwsUP/QMp5MmGjwFisXCNsIe686B7QKRaiOw62eJc2R7He8AuEC8T9OM4kRwDlecSn8mMpkoSB7QWtlJ+6XdLrJFPNvtrOfgfzS9/96Qrw9WlOslk68hMlhJeRb0s2YUD8tiV3UUkvbL1mfFoS4SI9U+rojS55KhUEJWHg1w7DjoOPoZmaIL2ChRupmvrFYNAGae1cxwG3Urh+t3wYlN3gpKsRDe5GOT7Wm2tr0ad3McCpDGUwSChX59BAJXe/MoLxkKScTrMyR8yMxHOF0b4zpVn5l7xB/o2Ik4zavx5q/0rGBMK2D+5d+gpQogKShoquTPsZUwO7sB5hYeH2hqGqpeGzZtb76E2zZYd18pJ0FsBudm5+KWjYdZ+vbtGrLxdTXJ1EEtzVXm0lscykTpqUucbXSa51dhStJvW2xEEz6p3rHo= + distributions: "sdist bdist_wheel" diff --git a/docs/installation.rst b/docs/installation.rst index a2dc665..52f2520 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -66,4 +66,26 @@ The most basic ``schema.py`` looks like this: schema = graphene.Schema(query=Query) -To learn how to extend the schema object for your project, read the basic tutorial. \ No newline at end of file +To learn how to extend the schema object for your project, read the basic tutorial. + +CSRF exempt +----------- + +If have enabled `CSRF protection `_ in your Django app +you will find that it prevents your API clients from POSTing to the ``graphql`` endpoint. You can either +update your API client to pass the CSRF token with each request (the Django docs have a guide on how to do that: https://docs.djangoproject.com/en/3.0/ref/csrf/#ajax) or you can exempt your Graphql endpoint from CSRF protection by wrapping the ``GraphQLView`` with the ``csrf_exempt`` +decorator: + +.. code:: python + + # urls.py + + from django.urls import path + from django.views.decorators.csrf import csrf_exempt + + from graphene_django.views import GraphQLView + + urlpatterns = [ + # ... + path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))), + ] diff --git a/graphene_django/__init__.py b/graphene_django/__init__.py index df58a5a..1ddc2cb 100644 --- a/graphene_django/__init__.py +++ b/graphene_django/__init__.py @@ -1,6 +1,6 @@ from .types import DjangoObjectType from .fields import DjangoConnectionField -__version__ = "2.7.1" +__version__ = "2.8.0" __all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"] diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 7acb573..b7bf049 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -66,28 +66,6 @@ class BaseDjangoFormMutation(ClientIDMutation): return kwargs -# class DjangoFormInputObjectTypeOptions(InputObjectTypeOptions): -# form_class = None - - -# class DjangoFormInputObjectType(InputObjectType): -# class Meta: -# abstract = True - -# @classmethod -# def __init_subclass_with_meta__(cls, form_class=None, -# only_fields=(), exclude_fields=(), _meta=None, **options): -# if not _meta: -# _meta = DjangoFormInputObjectTypeOptions(cls) -# assert isinstance(form_class, forms.Form), ( -# 'form_class must be an instance of django.forms.Form' -# ) -# _meta.form_class = form_class -# form = form_class() -# fields = fields_for_form(form, only_fields, exclude_fields) -# super(DjangoFormInputObjectType, cls).__init_subclass_with_meta__(_meta=_meta, fields=fields, **options) - - class DjangoFormMutationOptions(MutationOptions): form_class = None @@ -163,7 +141,9 @@ class DjangoModelFormMutation(BaseDjangoFormMutation): registry = get_global_registry() model_type = registry.get_type_for_model(model) - return_field_name = return_field_name + if not model_type: + raise Exception("No type registered for model: {}".format(model.__name__)) + if not return_field_name: model_name = model.__name__ return_field_name = model_name[:1].lower() + model_name[1:] diff --git a/graphene_django/forms/tests/test_mutation.py b/graphene_django/forms/tests/test_mutation.py index 6025784..cafec90 100644 --- a/graphene_django/forms/tests/test_mutation.py +++ b/graphene_django/forms/tests/test_mutation.py @@ -3,7 +3,8 @@ from django.test import TestCase from django.core.exceptions import ValidationError from py.test import raises -from graphene import ObjectType, String, Schema +from graphene import ObjectType, Schema, String, Field +from graphene_django import DjangoObjectType from graphene_django.tests.models import Film, FilmDetails, Pet from ...settings import graphene_settings @@ -29,6 +30,24 @@ class PetForm(forms.ModelForm): fields = "__all__" +class PetType(DjangoObjectType): + class Meta: + model = Pet + fields = "__all__" + + +class FilmType(DjangoObjectType): + class Meta: + model = Film + fields = "__all__" + + +class FilmDetailsType(DjangoObjectType): + class Meta: + model = FilmDetails + fields = "__all__" + + def test_needs_form_class(): with raises(Exception) as exc: @@ -186,34 +205,70 @@ class ModelFormMutationTests(TestCase): self.assertEqual(PetMutation._meta.return_field_name, "animal") self.assertIn("animal", PetMutation._meta.fields) - def test_model_form_mutation_mutate(self): + def test_model_form_mutation_mutate_existing(self): class PetMutation(DjangoModelFormMutation): + pet = Field(PetType) + class Meta: form_class = PetForm + class Mutation(ObjectType): + pet_mutation = PetMutation.Field() + + schema = Schema(query=MockQuery, mutation=Mutation) + pet = Pet.objects.create(name="Axel", age=10) - result = PetMutation.mutate_and_get_payload( - None, None, id=pet.pk, name="Mia", age=10 + result = schema.execute( + """ mutation PetMutation($pk: ID!) { + petMutation(input: { id: $pk, name: "Mia", age: 10 }) { + pet { + name + age + } + } + } + """, + variables={"pk": pet.pk}, ) + self.assertIs(result.errors, None) + self.assertEqual(result.data["petMutation"]["pet"], {"name": "Mia", "age": 10}) + self.assertEqual(Pet.objects.count(), 1) pet.refresh_from_db() self.assertEqual(pet.name, "Mia") - self.assertEqual(result.errors, []) - def test_model_form_mutation_updates_existing_(self): + def test_model_form_mutation_creates_new(self): class PetMutation(DjangoModelFormMutation): + pet = Field(PetType) + class Meta: form_class = PetForm - result = PetMutation.mutate_and_get_payload(None, None, name="Mia", age=10) + class Mutation(ObjectType): + pet_mutation = PetMutation.Field() + + schema = Schema(query=MockQuery, mutation=Mutation) + + result = schema.execute( + """ mutation PetMutation { + petMutation(input: { name: "Mia", age: 10 }) { + pet { + name + age + } + } + } + """ + ) + self.assertIs(result.errors, None) + self.assertEqual(result.data["petMutation"]["pet"], {"name": "Mia", "age": 10}) self.assertEqual(Pet.objects.count(), 1) pet = Pet.objects.get() self.assertEqual(pet.name, "Mia") self.assertEqual(pet.age, 10) - self.assertEqual(result.errors, []) def test_model_form_mutation_mutate_invalid_form(self): class PetMutation(DjangoModelFormMutation): diff --git a/graphene_django/tests/models.py b/graphene_django/tests/models.py index 14a8367..44a5d8a 100644 --- a/graphene_django/tests/models.py +++ b/graphene_django/tests/models.py @@ -64,6 +64,9 @@ class Reporter(models.Model): if self.reporter_type == 2: # quick and dirty way without enums self.__class__ = CNNReporter + def some_method(self): + return 123 + class CNNReporterManager(models.Manager): def get_queryset(self): diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 5e9d1c2..cb31a9c 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -315,7 +315,31 @@ def test_django_objecttype_fields_exclude_type_checking(): class Reporter2(DjangoObjectType): class Meta: model = ReporterModel - fields = "foo" + exclude = "foo" + + +@with_local_registry +def test_django_objecttype_fields_exclude_exist_on_model(): + with pytest.raises(Exception, match=r"Field .* doesn't exist"): + + class Reporter(DjangoObjectType): + class Meta: + model = ReporterModel + fields = ["first_name", "foo", "email"] + + with pytest.raises(Exception, match=r"Field .* doesn't exist"): + + class Reporter2(DjangoObjectType): + class Meta: + model = ReporterModel + exclude = ["first_name", "foo", "email"] + + with pytest.raises(Exception, match=r".* exists on model .* but it's not a field"): + + class Reporter3(DjangoObjectType): + class Meta: + model = ReporterModel + fields = ["first_name", "some_method", "email"] class TestDjangoObjectType: diff --git a/graphene_django/types.py b/graphene_django/types.py index ec426f1..4824c45 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -33,6 +33,24 @@ def construct_fields( ): _model_fields = get_model_fields(model) + # Validate the given fields against the model's fields. + model_field_names = set(field[0] for field in _model_fields) + for fields_list in (only_fields, exclude_fields): + if not fields_list: + continue + for name in fields_list: + if name in model_field_names: + continue + + if hasattr(model, name): + raise Exception( + '"{}" exists on model {} but it\'s not a field.'.format(name, model) + ) + else: + raise Exception( + 'Field "{}" doesn\'t exist on model {}.'.format(name, model) + ) + fields = OrderedDict() for name, field in _model_fields: is_not_in_only = only_fields and name not in only_fields