Delete mutation in Django GraphQL - django

The docs of Graphene-Django pretty much explains how to create and update an object. But how to delete it? I can imagine the query to look like
mutation mut{
deleteUser(id: 1){
user{
username
email
}
error
}
}
but i doubt that the correct approach is to write the backend code from scratch.

Something like this, where UsersMutations is part of your schema:
class DeleteUser(graphene.Mutation):
ok = graphene.Boolean()
class Arguments:
id = graphene.ID()
#classmethod
def mutate(cls, root, info, **kwargs):
obj = User.objects.get(pk=kwargs["id"])
obj.delete()
return cls(ok=True)
class UserMutations(object):
delete_user = DeleteUser.Field()

Here is a small model mutation you can add to your project based on the relay ClientIDMutation and graphene-django's SerializerMutation. I feel like this or something like this should be part of graphene-django.
import graphene
from graphene import relay
from graphql_relay.node.node import from_global_id
from graphene_django.rest_framework.mutation import SerializerMutationOptions
class RelayClientIdDeleteMutation(relay.ClientIDMutation):
id = graphene.ID()
message = graphene.String()
class Meta:
abstract = True
#classmethod
def __init_subclass_with_meta__(
cls,
model_class=None,
**options
):
_meta = SerializerMutationOptions(cls)
_meta.model_class = model_class
super(RelayClientIdDeleteMutation, cls).__init_subclass_with_meta__(
_meta=_meta, **options
)
#classmethod
def get_queryset(cls, queryset, info):
return queryset
#classmethod
def mutate_and_get_payload(cls, root, info, client_mutation_id):
id = int(from_global_id(client_mutation_id)[1])
cls.get_queryset(cls._meta.model_class.objects.all(),
info).get(id=id).delete()
return cls(id=client_mutation_id, message='deleted')
Use
class DeleteSomethingMutation(RelayClientIdDeleteMutation):
class Meta:
model_class = SomethingModel
You can also override get_queryset.

I made this library for simple model mutations: https://github.com/topletal/django-model-mutations , you can see how to delete user(s) in examples there
class UserDeleteMutation(mutations.DeleteModelMutation):
class Meta:
model = User

Going by the Python + Graphene hackernews tutorial, I derived the following implementation for deleting a Link object:
class DeleteLink(graphene.Mutation):
# Return Values
id = graphene.Int()
url = graphene.String()
description = graphene.String()
class Arguments:
id = graphene.Int()
def mutate(self, info, id):
link = Link.objects.get(id=id)
print("DEBUG: ${link.id}:${link.description}:${link.url}")
link.delete()
return DeleteLink(
id=id, # Strangely using link.id here does yield the correct id
url=link.url,
description=link.description,
)
class Mutation(graphene.ObjectType):
create_link = CreateLink.Field()
delete_link = DeleteLink.Field()

Related

Override Django (DRF) Serializer object GET

I'm trying to "inject" some raw sql into my DRF nested Serializer:
# SERIALIZERS
class CarSerializer(serializers.ModelSerializer):
class Meta:
model = Car
fields = '__all__'
class DriverSerializer(serializers.ModelSerializer):
car = CarSerializer() # <--- here I don't want to get the Car object but rather inject a raw sql.
class Meta:
model = Driver
fields = '__all__'
The SQL injection is needed to request for a specific version of the data since I'm using MariaDB versioning tables but this is not relevant. How do I override the method that gets the object from CarSerializer? Thank you.
This is untested but I think you want to override the __init__ in DriverSerializer and then load the result of your raw SQL via data, something like this:
class DriverSerializer(serializers.ModelSerializer):
[...]
def __init__(self, *args, **kwargs):
super(DriverSerializer, self).__init__(*args, **kwargs)
name_map = {'column_1': 'obj_attr_1', 'column_2': 'obj_attr_1', 'pk': 'id'}
raw = Car.objects.raw('SELECT ... FROM ...', translations=name_map)
data = {k: getattr(raw[0], k) for k in name_map.keys()}
self.car = CarSerializer(data=data)
You could define method under your model to get related Car
class Car(models.Model):
def current_car(self):
return Car.objects.raw('SELECT ... FROM ...')[0]
Then in serializer you could reuse following method
class DriverSerializer(serializers.ModelSerializer):
car = CarSerializer(source="current_car")
class Meta:
model = Driver
fields = (...)
Thank you everyone for your answers, I managed to make it work although my solution is not as clean as the one suggested from #yvesonline and #iklinak:
I first checked the official DRF documentation on overriding serializers: https://www.django-rest-framework.org/api-guide/serializers/#overriding-serialization-and-deserialization-behavior
In particular I was interested in the overriding of the method: .to_representation(self, instance) that controls the fetching of the object from the database:
from datetime import datetime as dt
from collections import OrderedDict
from rest_framework.relations import PKOnlyObject
from rest_framework.fields import SkipField, empty
def __init__(
self, instance=None, data=empty, asTime=str(dt.now()), **kwargs):
self.asTime = asTime
self.instance = instance
if data is not empty:
self.initial_data = data
self.partial = kwargs.pop('partial', False)
self._context = kwargs.pop('context', {})
kwargs.pop('many', None)
super().__init__(**kwargs)
def to_representation(self, instance):
# substitute instance with my raw query
# WARNING: self.asTime is a custom variable, check the
# __init__ method above!
instance = Car.objects.raw(
'''
select * from db_car
for system_time as of timestamp %s
where id=%s;
''', [self.asTime, instance.id])[0]
ret = OrderedDict()
fields = self._readable_fields
for field in fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
check_for_none = attribute.pk if isinstance(
attribute, PKOnlyObject) else attribute
if check_for_none is None:
ret[field.field_name] = None
else:
ret[field.field_name] = field.to_representation(attribute)
return ret
You can find the original code here: https://github.com/encode/django-rest-framework/blob/19655edbf782aa1fbdd7f8cd56ff9e0b7786ad3c/rest_framework/serializers.py#L335
Then finally in the DriverSerializer class:
class DriverSerializer(serializers.ModelSerializer):
car = CarSerializer(asTime='2021-02-05 14:34:00')
class Meta:
model = Driver
fields = '__all__'

How to mock the get_object method of a Django Rest Framework view?

I am playing with the build (as opposed to create) method of FactoryBoy in Django. This creates objects without storing them in the database.
Therefore, in order for the tests of my views to work, I need to patch the methods that touch the database.
Here is some code...
models.py:
class Book(models.Model):
title = models.CharField(max_length=100)
serializers.py:
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ('name')
views.py:
class BookViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Book.objects.all()
def get_serializer_class(self):
return BookSerializer
tests.py:
class BookFactory(factory.DjangoModelFactory):
title = factory.Faker('sentence', nb_words=4)
class Meta:
model = "Book"
def my_test():
client = APIClient()
books = BookFactory.build_batch(10)
list_url = reverse("books-list")
with patch.object(BookViewSet, "get_queryset", return_value=books):
list_url = reverse("books-list")
response = client.get(list_url)
# this works
assert response.content = <a list of books>
with patch.object(BookViewSet, "get_object", return_value=books[0]):
detail_url = reverse("books-detail", args=123)
response = client.get(detail_url)
# this is always empty..
assert response.content == <a book>
No matter what I try, the detail-view always returns empty JSON. Am I using patch wrong?
I had the same problem when retrieving a list (when get_queryset is used). Instead of passing a list, you should pass a queryset as return_value.
books = BookFactory.build_batch(2)
queryset = MockSet(books[0], books[1])
queryset.model = Book # fix django-filter issue " TypeError: issubclass() arg 1 must be a class"
with patch.object(BookViewSet, "get_queryset", return_value=queryset):
# do your stuff here

Django graphene with relay restricting queries access based on ID

I am trying to restrict single object queries to the user that created them.
Models.py
class Env(models.Model):
name = models.CharField(max_length=50)
user = models.ForeignKey(
User, on_delete=models.CASCADE)
description = TextField()
Schema.py
class EnvNode(DjangoObjectType):
class Meta:
model = Env
filter_fields = {
'name': ['iexact'],
'description': ['exact', 'icontains'],
}
interfaces = (relay.Node, )
Query(object):
env = relay.Node.Field(EnvNode)
all_envs = DjangoFilterConnectionField(EnvNode)
I tried adding a resolve query but it only worked for the "all_env" query with the filter and did not work for the single object query
def resolve_env(self, info):
env = Env.objects.filter(user = info.context.user.id)
if env is not None:
return env
else:
return None
Also tried adding a class method to the EnvNode as recommended here under filtering Node based ID access:
#classmethod
def get_node(context, cls, id, info):
try:
env = cls._meta.model.objects.get(id = id)
except cls._meta.model.DoesNotExist:
return None
if context.user == env.user:
return env
return None
but I got an error:
"message": "get_node() missing 1 required positional argument: 'info'",
Seems like the documentation is not correct, also your parameters are not in the correct order for the get_node method.
There are only three parameters for the call
The first one is your DjangoObjectType subclass: EnvNode
The second one is a graphql.execution.base.ResolveInfo which contains a reference to the context. You can get the user object from there.
The third one is the actual id for the object.
You should write it like this for the restriction to work:
#classmethod
def get_node(cls, info, id):
try:
env = cls._meta.model.objects.get(id=id, user=info.context.user)
except cls._meta.model.DoesNotExist:
return None
return env
Hope it helps.

Django REST Framework - is_valid() always passing and empty validated_data being returned

I have the following JSON GET request going to the server that defines a product configuration:
{'currency': ['"GBP"'], 'productConfig': ['[{"component":"c6ce9951","finish":"b16561c9"},{"component":"048f8bed","finish":"b4715cda"},{"component":"96801e41","finish":"8f90f764"},{"option":"6a202c62","enabled":false},{"option":"9aa498e0","enabled":true}]']}
I'm trying to validate this through DRF, and I have the following configuration:
views.py
class pricingDetail(generics.ListAPIView):
authentication_classes = (SessionAuthentication,)
permission_classes = (IsAuthenticated,)
parser_classes = (JSONParser,)
def get(self, request, *args, **kwargs):
pricingRequest = pricingRequestSerializer(data=request.query_params)
if pricingRequest.is_valid():
return Response('ok')
serializers.py
class pricingComponentSerializer(serializers.ModelSerializer):
class Meta:
model = Component
fields = ('sku',)
class pricingFinishSerializer(serializers.ModelSerializer):
class Meta:
model = Finish
fields = ('sku',)
class pricingOptionSerializer(serializers.ModelSerializer):
class Meta:
model = ProductOption
fields = ('sku',)
class pricingConfigSerializer(serializers.ModelSerializer):
finish = pricingFinishSerializer(read_only=True, many=True)
component = pricingComponentSerializer(read_only=True, many=True)
option = pricingOptionSerializer(read_only=True, many=True)
enabled = serializers.BooleanField(read_only=True)
class pricingCurrencySerializer(serializers.ModelSerializer):
class Meta:
model = Currency
fields = ('currencyCode',)
class pricingRequestSerializer(serializers.Serializer):
config = pricingConfigSerializer(read_only=True)
currency = pricingCurrencySerializer(read_only=True)
As you can see I'm trying to validate multiple models within the same request through the use of inline serializers.
My problem
The code above allow everything to pass through is_valid() (even when I make an invalid request, and, it also returns an empty validated_data (OrderedDict([])) value.
What am I doing wrong?
extra information
the JS generating the GET request is as follows:
this.pricingRequest = $.get(this.props.pricingEndpoint, { productConfig: JSON.stringify(this.state.productConfig), currency: JSON.stringify(this.state.selectedCurrency) }, function (returnedData, status) {
console.log(returnedData);
I currently don't have a computer to dig through the source but you might want to check the read_only parameters on your serializer. Afaik this only works for showing data in responses.
You can easily check by using ipdb (ipython debugger)
Just put:
import ipdb; ipdb.set_trace()
Somewhere you want to start debugging, start you server and start the request.

How do I customize the text of the select options in the api browser?

I am using rest_framework v3.1.3 in django 1.8. I am pretty new to django.
Here are the relevant model definitions
#python_2_unicode_compatible
class UserFitbit(models.Model):
user = models.OneToOneField(User, related_name='fituser')
fitbit_user = models.CharField(max_length=32)
auth_token = models.TextField()
auth_secret = models.TextField()
#this is a hack so that I can use this as a lookup field in the serializers
#property
def user__userid(self):
return self.user.id
def __str__(self):
return self.user.first_name + ' ' + self.user.last_name
def get_user_data(self):
return {
'user_key': self.auth_token,
'user_secret': self.auth_secret,
'user_id': self.fitbit_user,
'resource_owner_key': self.auth_token,
'resource_owner_secret': self.auth_secret,
'user_id': self.fitbit_user,
}
def to_JSON(self):
return json.dumps(self, default=lambda o: o.__dict__,
sort_keys=True, indent=4)
class Challenge(models.Model):
name=models.TextField()
status=models.TextField() #active, pending, ended, deleted
start_date=models.DateField()
end_date=models.DateField()
#members=models.ManyToManyField(UserFitbit)
members=models.ManyToManyField(User)
admin=models.ForeignKey(UserFitbit,related_name='admin')
#for each member get stats between the start and end dates
def memberstats(self):
stats = []
for member in self.members.all():
fbu = UserFitbit.objects.filter(user__id=member.id)
fu = UserData.objects.filter(userfitbit=fbu)
fu = fu.filter(activity_date__range=[self.start_date,self.end_date])
fu = fu.annotate(first_name=F('userfitbit__user__first_name'))
fu = fu.annotate(user_id=F('userfitbit__user__id'))
fu = fu.annotate(last_name=F('userfitbit__user__last_name'))
fu = fu.values('first_name','last_name','user_id')
fu = fu.annotate(total_distance=Sum('distance'),total_steps=Sum('steps'))
if fu:
stats.append(fu[0])
return stats
def __str__(self):
return 'Challenge:' + str(self.name)
class Meta:
ordering = ('-start_date','name')
And here is the serializer for the challenge
class ChallengeSerializer(serializers.ModelSerializer):
links = serializers.SerializerMethodField(read_only=True)
memberstats = MemberStatSerializer(read_only=True,many=True)
#these are user objects
#this should provide a hyperlink to each member
members = serializers.HyperlinkedRelatedField(
#queryset defines the valid selectable values
queryset=User.objects.all(),
view_name='user-detail',
lookup_field='pk',
many=True,
)
class Meta:
model=Challenge
fields = ('id','name','admin','status','start_date','end_date','members','links','memberstats',)
read_only_fields = ('memberstats','links',)
def get_links(self, obj) :
request = self.context['request']
return {
'self': reverse('challenge-detail',
kwargs={'pk':obj.pk},request=request),
}
As you can see the Challenge has a many to many relationship with User. This is the built in User model from django not UserFitBit defined here.
With these definitions when I go to the api browser for a challenge I need to be able to select the users based on their name, but the select only shows their User id property and the hyperlink url. I would like the members to be User objects, but I don't know how to change the text for the select options since I don't think I can change the built in User object. What is the best way to change the select box options to show the users name from the User object rather than the username field and hyperlink?
Here is an image:
I'm not sure if this is the best way but after reading DRF's source code, I would try this.
Subclass the HyperlinkedRelatedField and override the choices property.
import six
from collections import OrderedDict
class UserHyperLinkedRelatedField(serializers.HyperLinkedRelatedField):
#property
def choices(self):
queryset = self.get_queryset()
if queryset is None:
return {}
return OrderedDict([
(
six.text_type(self.to_representation(item)),
six.text_type(item.get_full_name())
)
for item in queryset
])
then would simply replace the field in the serializer.
members = UserHyperlinkedRelatedField(
queryset=User.objects.all(),
view_name='user-detail',
lookup_field='pk',
many=True,
)
The DRF docs also mentioned that there's a plan to add a public API to support customising HTML form generation in future releases.
Update
For DRF 3.2.2 or higher, there will be an available display_value method.
You can do
class UserHyperLinkedRelatedField(serializers.HyperLinkedRelatedField):
def display_value(self, instance):
return instance.get_full_name()
Because this is a many related field I also had to extend the ManyRelatedField and override the many_init method of the RelatedField to use that class. Can't say I understand all of this just yet, but it is working.
class UserManyRelatedField(serializers.ManyRelatedField):
#property
def choices(self):
queryset = self.child_relation.queryset
iterable = queryset.all() if (hasattr(queryset, 'all')) else queryset
items_and_representations = [
(item, self.child_relation.to_representation(item))
for item in iterable
]
return OrderedDict([
(
six.text_type(item_representation),
item.get_full_name() ,
)
for item, item_representation in items_and_representations
])
class UserHyperlinkedRelatedField(serializers.HyperlinkedRelatedField):
#classmethod
def many_init(cls, *args, **kwargs):
list_kwargs = {'child_relation': cls(*args, **kwargs)}
for key in kwargs.keys():
if key in MANY_RELATION_KWARGS:
list_kwargs[key] = kwargs[key]
return UserManyRelatedField(**list_kwargs)
members = UserHyperlinkedRelatedField(
queryset=User.objects.all(),
view_name='user-detail',
lookup_field='pk',
many=True,
)