# graphene-django-plus
[![build status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2F0soft%2Fgraphene-django-plus%2Fbadge%3Fref%3Dmaster&style=flat)](https://actions-badge.atrox.dev/0soft/graphene-django-plus/goto?ref=master)
[![docs status](https://img.shields.io/readthedocs/graphene-django-plus.svg)](https://graphene-django-plus.readthedocs.io)
[![coverage](https://img.shields.io/codecov/c/github/0soft/graphene-django-plus.svg)](https://codecov.io/gh/0soft/graphene-django-plus)
[![PyPI version](https://img.shields.io/pypi/v/graphene-django-plus.svg)](https://pypi.org/project/graphene-django-plus/)
![python version](https://img.shields.io/pypi/pyversions/graphene-django-plus.svg)
![django version](https://img.shields.io/pypi/djversions/graphene-django-plus.svg)
> ## DEPRECATION WARNING
>
> Graphene itself is abandoned and most users are migrating to other better alternatives, like
> [strawberry](https://github.com/strawberry-graphql/strawberry).
>
> For that reason this lib is being deprecated and new features will no longer be developed for it.
> Maintenance is still going to happen and PRs are still welcomed though.
>
> For anyone looking for alternatives, I created
> [strawberry-django-plus](https://github.com/blb-ventures/strawberry-django-plus) to use not
> only as a migration path to the projects I maintain, but also to add even more awesome features.
> Be sure to check it out!
Tools to easily create permissioned CRUD endpoints in [graphene-django](https://github.com/graphql-python/graphene-django).
## Install
```bash
pip install graphene-django-plus
```
To make use of everything this lib has to offer, it is recommended to install
both [graphene-django-optimizer](https://github.com/tfoxy/graphene-django-optimizer)
and [django-guardian](https://github.com/django-guardian/django-guardian).
```bash
pip install graphene-django-optimizer django-guardian
```
## What it does
- Provides some base types for Django Models to improve querying them with:
- Unauthenticated user handling
- Automatic optimization using [graphene-django-optimizer](https://github.com/tfoxy/graphene-django-optimizer)
- Permission handling for queries using the default [django permission system](https://docs.djangoproject.com/en/2.2/topics/auth/default/#topic-authorization)
- Object permission handling for queries using [django guardian](https://github.com/django-guardian/django-guardian)
- Relay id conversion so querying can use the global id instead of the model's id
- Provides a set of complete and simple CRUD mutations with:
- Unauthenticated user handling
- Permission handling using the default [django permission system](https://docs.djangoproject.com/en/2.2/topics/auth/default/#topic-authorization)
- Object permission handling using [django guardian](https://github.com/django-guardian/django-guardian)
- Automatic input generation based on the model (no need to write your own input type or use `django forms` and `drf serializers`)
- Automatic model validation based on the model's validators
- Very simple to create some quick CRUD endpoints for your models
- Easy to extend and override functionalities
- File upload handling
## What is included
Check the [docs](https://graphene-django-plus.readthedocs.io) for a complete
api documentation.
### Models
- `graphene_django_plus.models.GuardedModel`: A django model that can be used
either directly or as a mixin. It will provide a `.has_perm` method and a
`.objects.for_user` that will be used by `ModelType` described below to
check for object permissions.
### Types and Queries
- `graphene_django_plus.types.ModelType`: This enchances
`graphene_django_plus.DjangoModelType` by doing some automatic `prefetch`
optimization on setup and also checking for objects permissions on queries
when it inherits from `GuardedModel`.
- `graphene_django_plus.fields.CountableConnection`: This enchances
`graphene.relay.Connection` to provide a `total_count` attribute.
Here is an example describing how to use those:
```py
import graphene
from graphene import relay
from graphene_django.fields import DjangoConnectionField
from graphene_django_plus.models import GuardedModel
from graphene_django_plus.types import ModelType
from graphene_django_plus.fields import CountableConnection
class MyModel(GuardedModel):
class Meta:
# guardian permissions for this model
permissions = [
('can_read', "Can read the this object's info."),
]
name = models.CharField(max_length=255)
class MyModelType(ModelType):
class Meta:
model = MyModel
interfaces = [relay.Node]
# Use our CountableConnection
connection_class = CountableConnection
# When adding this to a query, only objects with a `can_read`
# permission to the request's user will be allowed to return to him
# Note that `can_read` was defined in the model.
# If the model doesn't inherid from `GuardedModel`, `guardian` is not
# installed or this list is empty, any object will be allowed.
# This is empty by default
object_permissions = [
'can_read',
]
# If unauthenticated users should be allowed to retrieve any object
# of this type. This is not dependent on `GuardedModel` and neither
# `guardian` and is defined as `False` by default
public = False
# A list of Django model permissions to check. Different from
# object_permissions, this uses the basic Django's permission system
# and thus is not dependent on `GuardedModel` and neither `guardian`.
# This is an empty list by default.
permissions = []
class Query(graphene.ObjectType):
my_models = DjangoConnectionField(MyModelType)
my_model = relay.Node.Field(MyModelType)
```
This can be queried like:
```graphql
# All objects that the user has permission to see
query {
myModels {
totalCount
edges {
node {
id
name
}
}
}
}
# Single object if the user has permission to see it
query {
myModel(id: "<relay global ID>") {
id
name
}
}
```
### Mutations
- `graphene_django_plus.mutations.BaseMutation`: Base mutation using `relay`
and some basic permission checking. Just override its `.perform_mutation` to
perform the mutation.
- `graphene_django_plus.mutations.ModelMutation`: Model mutation capable of
both creating and updating a model based on the existence of an `id`
attribute in the input. All the model's fields will be automatically read
from Django, inserted in the input type and validated.
- `graphene_django_plus.mutations.ModelCreateMutation`: A `ModelMutation`
enforcing a "create only" rule by excluding the `id` field from the input.
- `graphene_django_plus.mutations.ModelUpdateMutation`: A `ModelMutation`
enforcing a "update only" rule by making the `id` field required in the
input.
- `graphene_django_plus.mutations.ModelDeleteMutation`: A mutation that will
receive only the model's id and will delete it (if given permission, of
course).
Here is an example describing how to use those:
```py
import graphene
from graphene import relay
from graphene_django_plus.models import GuardedModel
from graphene_django_plus.types import ModelType
from graphene_django_plus.mutations import (
ModelCreateMutation,
ModelUpdateMutation,
ModelDeleteMutation,
)
class MyModel(GuardedModel):
class Meta:
# guardian permissions for this model
permissions = [
('can_write', "Can update this object's info."),
]
name = models.CharField(max_length=255)
class MyModelType(ModelType):
class Meta:
model = MyModel
interfaces = [relay.Node]
class MyModelUpdateMutation(ModelUpdateMutation):
class Meta:
model = MyModel
# Make sure only users with the given permissions can modify the
# object.
# If the model doesn't inherid from `GuardedModel`, `guardian` is not
# installed on this list is empty, any object will be allowed.
# This is empty by default.
object_permissions = [
'can_write',
]
# If unauthenticated users should be allowed to retrieve any object
# of this type. This is not dependent on `GuardedModel` and neither
# `guardian` and is defined as `False` by default
public = False
# A list of Django model permissions to check. Different from
# object_permissions, this uses the basic Django's permission system
# and thus is not dependent on `GuardedModel` and neither `guardian`.
# This is an empty list by default.
permissions = []
class MyModelDeleteMutation(ModelDeleteMutation):
class Meta:
model = MyModel
object_permissions = [
'can_write',
]
class MyModelCreateMutation(ModelCreateMutation):
class Meta:
model = MyModel
@classmethod
def after_save(cls, info, instance, cleaned_input=None):
# If the user created the object, allow him to modify it
assign_perm('can_write', info.context.user, instance)
class Mutation(graphene.ObjectType):
my_model_create = MyModelCreateMutation.Field()
my_model_update = MyModelUpdateMutation.Field()
my_model_delete = MyModelDeleteMutation.Field()
```
This can be used to create/update/delete like:
```graphql
# Create mutation
mutation {
myModelCreate(input: { name: "foobar" }) {
myModel {
name
}
errors {
field
message
}
}
}
# Update mutation
mutation {
myModelUpdate(input: { id: "<relay global ID>", name: "foobar" }) {
myModel {
name
}
errors {
field
message
}
}
}
# Delete mutation
mutation {
myModelDelete(input: { id: "<relay global ID>" }) {
myModel {
name
}
errors {
field
message
}
}
}
```
Any validation errors will be presented in the `errors` return value.
To turn off auto related relations addition to the mutation input - set global
`MUTATIONS_INCLUDE_REVERSE_RELATIONS` parameter to `False` in your
`settings.py`:
```
GRAPHENE_DJANGO_PLUS = {
'MUTATIONS_INCLUDE_REVERSE_RELATIONS': False
}
```
Note: in case reverse relation does not have `related_name` attribute set -
mutation input will be generated as Django itself is generating by appending
`_set` to the lower cased model name - `modelname_set`
## License
This project is licensed under MIT licence (see `LICENSE` for more info)
## Contributing
Make sure to have [poetry](https://python-poetry.org/) installed.
Install dependencies with:
```bash
poetry install
```
Run the testsuite with:
```bash
poetry run pytest
```
Feel free to fork the project and send me pull requests with new features,
corrections and translations. We'll gladly merge them and release new versions
ASAP.
Raw data
{
"_id": null,
"home_page": "https://github.com/0soft/graphene-django-plus",
"name": "graphene-django-plus",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.8,<4.0",
"maintainer_email": "",
"keywords": "graphene,django,graphql,crud,permissions",
"author": "Thiago Bellini Ribeiro",
"author_email": "thiago@bellini.dev",
"download_url": "https://files.pythonhosted.org/packages/ef/76/4ecc6be72bf3dffe947ff930a13c9aa7a0dd29b2c2dfdb6bcafef72bc89c/graphene_django_plus-5.1.tar.gz",
"platform": null,
"description": "# graphene-django-plus\n\n[![build status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2F0soft%2Fgraphene-django-plus%2Fbadge%3Fref%3Dmaster&style=flat)](https://actions-badge.atrox.dev/0soft/graphene-django-plus/goto?ref=master)\n[![docs status](https://img.shields.io/readthedocs/graphene-django-plus.svg)](https://graphene-django-plus.readthedocs.io)\n[![coverage](https://img.shields.io/codecov/c/github/0soft/graphene-django-plus.svg)](https://codecov.io/gh/0soft/graphene-django-plus)\n[![PyPI version](https://img.shields.io/pypi/v/graphene-django-plus.svg)](https://pypi.org/project/graphene-django-plus/)\n![python version](https://img.shields.io/pypi/pyversions/graphene-django-plus.svg)\n![django version](https://img.shields.io/pypi/djversions/graphene-django-plus.svg)\n\n> ## DEPRECATION WARNING\n>\n> Graphene itself is abandoned and most users are migrating to other better alternatives, like\n> [strawberry](https://github.com/strawberry-graphql/strawberry).\n>\n> For that reason this lib is being deprecated and new features will no longer be developed for it.\n> Maintenance is still going to happen and PRs are still welcomed though.\n>\n> For anyone looking for alternatives, I created\n> [strawberry-django-plus](https://github.com/blb-ventures/strawberry-django-plus) to use not\n> only as a migration path to the projects I maintain, but also to add even more awesome features.\n> Be sure to check it out!\n\nTools to easily create permissioned CRUD endpoints in [graphene-django](https://github.com/graphql-python/graphene-django).\n\n## Install\n\n```bash\npip install graphene-django-plus\n```\n\nTo make use of everything this lib has to offer, it is recommended to install\nboth [graphene-django-optimizer](https://github.com/tfoxy/graphene-django-optimizer)\nand [django-guardian](https://github.com/django-guardian/django-guardian).\n\n```bash\npip install graphene-django-optimizer django-guardian\n```\n\n## What it does\n\n- Provides some base types for Django Models to improve querying them with:\n - Unauthenticated user handling\n - Automatic optimization using [graphene-django-optimizer](https://github.com/tfoxy/graphene-django-optimizer)\n - Permission handling for queries using the default [django permission system](https://docs.djangoproject.com/en/2.2/topics/auth/default/#topic-authorization)\n - Object permission handling for queries using [django guardian](https://github.com/django-guardian/django-guardian)\n - Relay id conversion so querying can use the global id instead of the model's id\n- Provides a set of complete and simple CRUD mutations with:\n - Unauthenticated user handling\n - Permission handling using the default [django permission system](https://docs.djangoproject.com/en/2.2/topics/auth/default/#topic-authorization)\n - Object permission handling using [django guardian](https://github.com/django-guardian/django-guardian)\n - Automatic input generation based on the model (no need to write your own input type or use `django forms` and `drf serializers`)\n - Automatic model validation based on the model's validators\n- Very simple to create some quick CRUD endpoints for your models\n- Easy to extend and override functionalities\n- File upload handling\n\n## What is included\n\nCheck the [docs](https://graphene-django-plus.readthedocs.io) for a complete\napi documentation.\n\n### Models\n\n- `graphene_django_plus.models.GuardedModel`: A django model that can be used\n either directly or as a mixin. It will provide a `.has_perm` method and a\n `.objects.for_user` that will be used by `ModelType` described below to\n check for object permissions.\n\n### Types and Queries\n\n- `graphene_django_plus.types.ModelType`: This enchances\n `graphene_django_plus.DjangoModelType` by doing some automatic `prefetch`\n optimization on setup and also checking for objects permissions on queries\n when it inherits from `GuardedModel`.\n\n- `graphene_django_plus.fields.CountableConnection`: This enchances\n `graphene.relay.Connection` to provide a `total_count` attribute.\n\nHere is an example describing how to use those:\n\n```py\nimport graphene\nfrom graphene import relay\nfrom graphene_django.fields import DjangoConnectionField\n\nfrom graphene_django_plus.models import GuardedModel\nfrom graphene_django_plus.types import ModelType\nfrom graphene_django_plus.fields import CountableConnection\n\n\nclass MyModel(GuardedModel):\n class Meta:\n # guardian permissions for this model\n permissions = [\n ('can_read', \"Can read the this object's info.\"),\n ]\n\n name = models.CharField(max_length=255)\n\n\nclass MyModelType(ModelType):\n class Meta:\n model = MyModel\n interfaces = [relay.Node]\n\n # Use our CountableConnection\n connection_class = CountableConnection\n\n # When adding this to a query, only objects with a `can_read`\n # permission to the request's user will be allowed to return to him\n # Note that `can_read` was defined in the model.\n # If the model doesn't inherid from `GuardedModel`, `guardian` is not\n # installed or this list is empty, any object will be allowed.\n # This is empty by default\n object_permissions = [\n 'can_read',\n ]\n\n # If unauthenticated users should be allowed to retrieve any object\n # of this type. This is not dependent on `GuardedModel` and neither\n # `guardian` and is defined as `False` by default\n public = False\n\n # A list of Django model permissions to check. Different from\n # object_permissions, this uses the basic Django's permission system\n # and thus is not dependent on `GuardedModel` and neither `guardian`.\n # This is an empty list by default.\n permissions = []\n\n\nclass Query(graphene.ObjectType):\n my_models = DjangoConnectionField(MyModelType)\n my_model = relay.Node.Field(MyModelType)\n```\n\nThis can be queried like:\n\n```graphql\n# All objects that the user has permission to see\nquery {\n myModels {\n totalCount\n edges {\n node {\n id\n name\n }\n }\n }\n}\n\n# Single object if the user has permission to see it\nquery {\n myModel(id: \"<relay global ID>\") {\n id\n name\n }\n}\n```\n\n### Mutations\n\n- `graphene_django_plus.mutations.BaseMutation`: Base mutation using `relay`\n and some basic permission checking. Just override its `.perform_mutation` to\n perform the mutation.\n\n- `graphene_django_plus.mutations.ModelMutation`: Model mutation capable of\n both creating and updating a model based on the existence of an `id`\n attribute in the input. All the model's fields will be automatically read\n from Django, inserted in the input type and validated.\n\n- `graphene_django_plus.mutations.ModelCreateMutation`: A `ModelMutation`\n enforcing a \"create only\" rule by excluding the `id` field from the input.\n\n- `graphene_django_plus.mutations.ModelUpdateMutation`: A `ModelMutation`\n enforcing a \"update only\" rule by making the `id` field required in the\n input.\n\n- `graphene_django_plus.mutations.ModelDeleteMutation`: A mutation that will\n receive only the model's id and will delete it (if given permission, of\n course).\n\nHere is an example describing how to use those:\n\n```py\nimport graphene\nfrom graphene import relay\n\nfrom graphene_django_plus.models import GuardedModel\nfrom graphene_django_plus.types import ModelType\nfrom graphene_django_plus.mutations import (\n ModelCreateMutation,\n ModelUpdateMutation,\n ModelDeleteMutation,\n)\n\n\nclass MyModel(GuardedModel):\n class Meta:\n # guardian permissions for this model\n permissions = [\n ('can_write', \"Can update this object's info.\"),\n ]\n\n name = models.CharField(max_length=255)\n\n\nclass MyModelType(ModelType):\n class Meta:\n model = MyModel\n interfaces = [relay.Node]\n\n\nclass MyModelUpdateMutation(ModelUpdateMutation):\n class Meta:\n model = MyModel\n\n # Make sure only users with the given permissions can modify the\n # object.\n # If the model doesn't inherid from `GuardedModel`, `guardian` is not\n # installed on this list is empty, any object will be allowed.\n # This is empty by default.\n object_permissions = [\n 'can_write',\n ]\n\n # If unauthenticated users should be allowed to retrieve any object\n # of this type. This is not dependent on `GuardedModel` and neither\n # `guardian` and is defined as `False` by default\n public = False\n\n # A list of Django model permissions to check. Different from\n # object_permissions, this uses the basic Django's permission system\n # and thus is not dependent on `GuardedModel` and neither `guardian`.\n # This is an empty list by default.\n permissions = []\n\n\nclass MyModelDeleteMutation(ModelDeleteMutation):\n class Meta:\n model = MyModel\n object_permissions = [\n 'can_write',\n ]\n\n\nclass MyModelCreateMutation(ModelCreateMutation):\n class Meta:\n model = MyModel\n\n @classmethod\n def after_save(cls, info, instance, cleaned_input=None):\n # If the user created the object, allow him to modify it\n assign_perm('can_write', info.context.user, instance)\n\n\nclass Mutation(graphene.ObjectType):\n my_model_create = MyModelCreateMutation.Field()\n my_model_update = MyModelUpdateMutation.Field()\n my_model_delete = MyModelDeleteMutation.Field()\n```\n\nThis can be used to create/update/delete like:\n\n```graphql\n# Create mutation\nmutation {\n myModelCreate(input: { name: \"foobar\" }) {\n myModel {\n name\n }\n errors {\n field\n message\n }\n }\n}\n\n# Update mutation\nmutation {\n myModelUpdate(input: { id: \"<relay global ID>\", name: \"foobar\" }) {\n myModel {\n name\n }\n errors {\n field\n message\n }\n }\n}\n\n# Delete mutation\nmutation {\n myModelDelete(input: { id: \"<relay global ID>\" }) {\n myModel {\n name\n }\n errors {\n field\n message\n }\n }\n}\n```\n\nAny validation errors will be presented in the `errors` return value.\n\nTo turn off auto related relations addition to the mutation input - set global\n`MUTATIONS_INCLUDE_REVERSE_RELATIONS` parameter to `False` in your\n`settings.py`:\n\n```\nGRAPHENE_DJANGO_PLUS = {\n 'MUTATIONS_INCLUDE_REVERSE_RELATIONS': False\n}\n```\n\nNote: in case reverse relation does not have `related_name` attribute set -\nmutation input will be generated as Django itself is generating by appending\n`_set` to the lower cased model name - `modelname_set`\n\n## License\n\nThis project is licensed under MIT licence (see `LICENSE` for more info)\n\n## Contributing\n\nMake sure to have [poetry](https://python-poetry.org/) installed.\n\nInstall dependencies with:\n\n```bash\npoetry install\n```\n\nRun the testsuite with:\n\n```bash\npoetry run pytest\n```\n\nFeel free to fork the project and send me pull requests with new features,\ncorrections and translations. We'll gladly merge them and release new versions\nASAP.\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Tools to easily create permissioned CRUD endpoints in graphene.",
"version": "5.1",
"project_urls": {
"Documentation": "https://graphene-django-plus.readthedocs.io",
"Homepage": "https://github.com/0soft/graphene-django-plus",
"Repository": "https://github.com/0soft/graphene-django-plus"
},
"split_keywords": [
"graphene",
"django",
"graphql",
"crud",
"permissions"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "f3f497d10886b6e6d8a1b0ba14d50572899c58494d6654bcd10c1929fffef4c6",
"md5": "921140234d35f0ef8869b7e20057eb5f",
"sha256": "428281136e281656a02f496b5ef5b56b9b4f92ebf0afd8a04e183ebc8880c2a4"
},
"downloads": -1,
"filename": "graphene_django_plus-5.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "921140234d35f0ef8869b7e20057eb5f",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8,<4.0",
"size": 26465,
"upload_time": "2023-06-20T18:10:17",
"upload_time_iso_8601": "2023-06-20T18:10:17.716971Z",
"url": "https://files.pythonhosted.org/packages/f3/f4/97d10886b6e6d8a1b0ba14d50572899c58494d6654bcd10c1929fffef4c6/graphene_django_plus-5.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "ef764ecc6be72bf3dffe947ff930a13c9aa7a0dd29b2c2dfdb6bcafef72bc89c",
"md5": "1093fcfbdedfc0a685169583e1bbef89",
"sha256": "2923d196ab3f392aab17b06a8a19d836d6eb82df932e4f0aaec2212fb6827f2e"
},
"downloads": -1,
"filename": "graphene_django_plus-5.1.tar.gz",
"has_sig": false,
"md5_digest": "1093fcfbdedfc0a685169583e1bbef89",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8,<4.0",
"size": 25039,
"upload_time": "2023-06-20T18:10:19",
"upload_time_iso_8601": "2023-06-20T18:10:19.325302Z",
"url": "https://files.pythonhosted.org/packages/ef/76/4ecc6be72bf3dffe947ff930a13c9aa7a0dd29b2c2dfdb6bcafef72bc89c/graphene_django_plus-5.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-06-20 18:10:19",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "0soft",
"github_project": "graphene-django-plus",
"travis_ci": false,
"coveralls": true,
"github_actions": true,
"lcname": "graphene-django-plus"
}