Repository: graphql-python/graphene-django

Issue:
Slightly incorrect Travis pipeline
In current implementation, when testing a tagged commit, PyPI release would be triggered after the first success. This is incorrect, since what works on Python 3.5 might not work on Python 3.6 (not even talking about Python 2, which is the first one to be tested in the list).

Thus, I suggest to set ``fast_finish`` to ``false`` so that release attempt would be made only once after all checks are finished (all tests passed).

```yaml
# ...
matrix:
  fast_finish: false
# ...
```

Alternatively, if my proposal above isn't acceptable (for whatever reason), then I suggest to add ``skip_existing: true`` to the ``deploy`` pipeline. The reason: Travis would report failures even if tests pass when testing for the second time (and further on). Currently, once Python 2 tests pass, release is made, then testing on Python 3.5 starts, tests pass, but the final step fails, since package is already on the PyPI. With ``skip_existing: true`` that would no longer happen.

```yaml
# ...
deploy:
  skip_existing: true
  provider: pypi
# ...
```

Let me know if I shall submit a PR (please, mention which fix would be more appropriate/acceptable).

Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index 3531b56..1718d79 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,14 @@ 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
+      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/examples/cookbook/requirements.txt b/examples/cookbook/requirements.txt
index 3209a5e..b1baa57 100644
--- a/examples/cookbook/requirements.txt
+++ b/examples/cookbook/requirements.txt
@@ -1,5 +1,5 @@
 graphene
 graphene-django
 graphql-core>=2.1rc1
-django==2.2.8
+django==3.0
 django-filter>=2
diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py
index d9c695e..060b370 100644
--- a/graphene_django/rest_framework/mutation.py
+++ b/graphene_django/rest_framework/mutation.py
@@ -102,8 +102,10 @@ class SerializerMutation(ClientIDMutation):
                 instance = get_object_or_404(
                     model_class, **{lookup_field: input[lookup_field]}
                 )
+                partial = True
             elif "create" in cls._meta.model_operations:
                 instance = None
+                partial = False
             else:
                 raise Exception(
                     'Invalid update operation. Input parameter "{}" required.'.format(
@@ -115,6 +117,7 @@ class SerializerMutation(ClientIDMutation):
                 "instance": instance,
                 "data": input,
                 "context": {"request": info.context},
+                "partial": partial,
             }
 
         return {"data": input, "context": {"request": info.context}}
diff --git a/graphene_django/static/graphene_django/graphiql.js b/graphene_django/static/graphene_django/graphiql.js
index 2be7e3c..e38cd62 100644
--- a/graphene_django/static/graphene_django/graphiql.js
+++ b/graphene_django/static/graphene_django/graphiql.js
@@ -3,8 +3,11 @@
   // Parse the cookie value for a CSRF token
   var csrftoken;
   var cookies = ('; ' + document.cookie).split('; csrftoken=');
-  if (cookies.length == 2)
+  if (cookies.length == 2) {
     csrftoken = cookies.pop().split(';').shift();
+  } else {
+    csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
+  }
 
   // Collect the URL parameters
   var parameters = {};
diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html
index d0fb5a8..a0d0e1a 100644
--- a/graphene_django/templates/graphene/graphiql.html
+++ b/graphene_django/templates/graphene/graphiql.html
@@ -31,6 +31,7 @@ add "&raw" to the end of the URL within a browser.
           crossorigin="anonymous"></script>
 </head>
 <body>
+  {% csrf_token %}
   <script src="{% static 'graphene_django/graphiql.js' %}"></script>
 </body>
 </html>
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


Test patch:
diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py
index 9d8b950..bfb247d 100644
--- a/graphene_django/rest_framework/tests/test_mutation.py
+++ b/graphene_django/rest_framework/tests/test_mutation.py
@@ -183,6 +183,16 @@ def test_model_update_mutate_and_get_payload_success():
     assert result.cool_name == "New Narf"
 
 
+@mark.django_db
+def test_model_partial_update_mutate_and_get_payload_success():
+    instance = MyFakeModel.objects.create(cool_name="Narf")
+    result = MyModelMutation.mutate_and_get_payload(
+        None, mock_info(), **{"id": instance.id}
+    )
+    assert result.errors is None
+    assert result.cool_name == "Narf"
+
+
 @mark.django_db
 def test_model_invalid_update_mutate_and_get_payload_success():
     class InvalidModelMutation(SerializerMutation):
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: