Django graphene with relay restricting queries access based on ID - django

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.

Related

Delete mutation in Django GraphQL

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()

Django rest framework not creating object with FK to a model with unique=True field

I have two models like this:
class Sector(models.Model):
name = models.CharField(max_length=100, db_index=True, unique=True) # HERE IF I REMOVE unique=True, it works correctly
class Address(models.Model):
...
sector = models.ForeignKey(Sector, null=True, blank=True)
And a serializer for the Address model:
In the view, I have this:
address_serialized = AddressSerializer(data=request.data)
if address_serialized.is_valid():
address_serialized.save(client=client)
It never gets to the create function. I have a serialized with a create function that looks like this:
class AddressSerializer(serializers.ModelSerializer):
city_gps = CitySerializer(required=False)
sector = SectorSerializer(required=False)
class Meta:
model = Address
fields = (..., "sector")
def create(self, validated_data):
...
sector_dict = validated_data.get("sector", None)
sector = None
if sector_dict and "name" in sector_dict and city_gps:
if Sector.objects.filter(name=sector_dict["name"], city=city_gps).exists():
sector = Sector.objects.get(name=sector_dict["name"], city=city_gps)
# pdb.set_trace()
if "sector" in validated_data:
validated_data.pop("sector")
if "city_gps" in validated_data:
validated_data.pop("city_gps")
address = Address.objects.create(sector=sector, city_gps=city_gps, **validated_data)
return address
The code never touches this function, is_valid() returns False. And the message is
{"sector":{"name":["sector with this name already exists."]}}
I need to be able to create a new address with FK to the already existing sector. How can I achieve that? Any advice will help.
EDIT
The view looks like this:
class ClientProfileAddressCreateView(APIView):
# throttle_scope = '1persecond'
renderer_classes = (JSONRenderer,)
permission_classes = (IsAuthenticated,)
def post(self, request):
try:
client = Client.objects.get(user=request.user)
except ObjectDoesNotExist:
return Response({"error": "A client profile for the logged user does not exit"},
status=status.HTTP_404_NOT_FOUND)
address_serialized = AddressSerializer(data=request.data)
print("address_serialized.is_valid: %s" % address_serialized.is_valid()) # Returns False when unique=True in models
if address_serialized.is_valid():
# print("address_serialized: %s" % address_serialized.data)
address_serialized.save(client=client)
else:
return Response(data=address_serialized.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(data=address_serialized.data, status=status.HTTP_201_CREATED)
This is a known issue with nested serializers and unique constraints.
Really awesome thing to always do is actually print the Serializer - that can give you a lot of extra info.
When you have a json like this:
{
"Sector": {
"name": "Sector XYZ"
},
"address_line_one": “Some Random Address”
}
Django REST framework does not know whether you're creating or getting the Sector object, thus it forces validation on every request.
What you need to do is the following:
class SectorSerializer(serializers.ModelSerializer):
# Your fields.
class Meta:
model = Address
fields = ("Your Fields",)
extra_kwargs = {
'name': {
'validators': [],
}
}
Then to handle validation you would need to redo the create/update part to fit the uniqueness constraint and raise exception/validation error.
I hope this helps.
Helpful links: This SO Answer and Dealing with unique constraints in nested serializers
EDIT :
As per cezar's request: I will add how it might look like to override the create method of the serializer. I have not tried this code, but the logic goes like this.
class SectorSerializer(serializers.ModelSerializer):
# Your fields.
class Meta:
model = Address
fields = ("Your Fields",)
extra_kwargs = {
'name': {
'validators': [],
}
}
def create(self, validated_data):
raise_errors_on_nested_writes('create', self, validated_data)
ModelClass = self.Meta.model
info = model_meta.get_field_info(ModelClass)
many_to_many = {}
for field_name, relation_info in info.relations.items():
if relation_info.to_many and (field_name in validated_data):
many_to_many[field_name] = validated_data.pop(field_name)
# FIELD CHECK
your_field = validated_data.get("your_field","") # or validated_data["your_field"]
try:
YourModel.objects.filter(your_check=your_field).get()
raise ValidationError("Your error")
except YourModel.DoesNotExist:
# if it doesn't exist it means that no model containing that field exists so pass it. You can use YourQuerySet.exists() but then the logic changes
pass
try:
instance = ModelClass.objects.create(**validated_data)
except TypeError:
tb = traceback.format_exc()
msg = (
'Got a `TypeError` when calling `%s.objects.create()`. '
'This may be because you have a writable field on the '
'serializer class that is not a valid argument to '
'`%s.objects.create()`. You may need to make the field '
'read-only, or override the %s.create() method to handle '
'this correctly.\nOriginal exception was:\n %s' %
(
ModelClass.__name__,
ModelClass.__name__,
self.__class__.__name__,
tb
)
)
raise TypeError(msg)
# Save many-to-many relationships after the instance is created.
if many_to_many:
for field_name, value in many_to_many.items():
field = getattr(instance, field_name)
field.set(value)
return instance

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,
)

django-tastypie: linking a ModelResource to a Resource

I'm currently trying django-tastypie to design a RESTful api. I'm facing a problem:
# the RevisionObject retrieve commits info through pysvn
# This Resource is fully functionnal (RevisionObject code is not here)
class RevisionResource(Resource):
id = fields.CharField(attribute='revision')
description = fields.CharField(attribute='message')
author = fields.CharField(attribute='author')
changed_path = fields.ListField(attribute='changed_paths')
class Meta:
object_class = RevisionObject
allowed_methods = ['get']
resource_name = 'revision'
class RevisionToApplyResource(ModelResource):
#### here's the problem
revision = fields.ToManyField(RevisionResource, 'revision')
####
class Meta:
queryset = RevisionToApply.objects.all()
In my models.py I have:
class RevisionToApply(models.Model):
patch = models.ForeignKey(PatchRequest)
revision = models.PositiveIntegerField()
applied = models.BooleanField(default = False)
My problem is that the RevisionToApply models (for django) uses an int to the revision.
How can I tell tastypie to use the revision field of RevisionToApplyResource as a pointer to a RevisionResource? If the ToXxxxField are only for linking with django models, what is the perfect moment to insert the ResourceObject?
thanks.
class NoForeignKeyToOneField(ToOneField):
def dehydrate(self, bundle):
try:
obj_key = getattr(bundle.obj, self.attribute)
foreign_obj = self.to_class().obj_get(pk=obj_key)
except ObjectDoesNotExist:
foreign_obj= None
if not foreign_obj:
if not self.null:
raise ApiFieldError("The model '%r' has an empty attribute"
"'%s' and doesn't allow null value." % (bundle.obj,
self.attribute))
return None
self.fk_resource = self.get_related_resource(foreign_obj)
fk_bundle = Bundle(obj=foreign_obj, request=bundle.request)
return self.dehydrate_related(fk_bundle, self.fk_resource)
Here's how I would do it. Taking a look at how the ToOneField class works, you'll notice that the hydrate/dehydrate method pair takes care of getting and setting the actual related instance. By subclassing ToOneField and overriding these two methods, you can get the benefit of Tastypie's automated resource handling without an actual foreign key.
(I'm referring to ToOneField rather than ToManyField because in your model, a given RevisionToApply can only point to one revision, it seems.)
It would look something like this:
class NoForeignKeyToOneField(ToOneField):
def dehydrate(self, bundle):
# Look up the related object manually
try:
obj_key = getattr(bundle.obj, self.attribute)
###
# Get the revision object here. If you want to make it generic,
# maybe pass a callable on __init__ that can be invoked here
###
foreign_obj = revision_object
except ObjectDoesNotExist:
foreign_obj = None
# The rest remains the same
if not foreign_obj:
if not self.null:
raise ApiFieldError("The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (bundle.obj, self.attribute))
return None
self.fk_resource = self.get_related_resource(foreign_obj)
fk_bundle = Bundle(obj=foreign_obj, request=bundle.request)
return self.dehydrate_related(fk_bundle, self.fk_resource)
def hydrate(self, bundle):
value = super(NoForeignKeyToOneField, self).hydrate(bundle)
if value is None:
return value
# Here, don't return the full resource, only the primary key
related_resource = self.build_related_resource(value, request=bundle.request)
return related_resource.pk
And then use this field type in your resource rather than the basic ToOneField. I haven't tested it , but I believe the approach is sound, simple and it'll get the job done.

Django - Overriding get_or_create with models.py

I have a class in which I want to override the get_or_create method. Basically if my class doesn't store the answer I want it do some process to get the answer and it's not provided. The method is really a get_or_retrieve method. So here's the class:
class P4User(models.Model):
user = models.CharField(max_length=100, primary_key=True)
fullname = models.CharField(max_length=256)
email = models.EmailField()
access = models.DateField(auto_now_add=True)
update = models.DateField(auto_now_add=True)
#classmethod
def get_or_retrieve(self, username, auto_now_add=False):
try:
return self.get(user=username), False
except self.model.DoesNotExist:
import P4
import datetime
from django.db import connection, transaction, IntegrityError
p4 = P4.P4().connect()
kwargs = p4.run(("user", "-o", username))[0]
p4.disconnect()
params = dict( [(k.lower(),v) for k, v in kwargs.items()])
obj = self.model(**params)
sid = transaction.savepoint()
obj.save(force_insert=True)
transaction.savepoint_commit(sid)
return obj, True
except IntegrityError, e:
transaction.savepoint_rollback(sid)
try:
return self.get(**kwargs), False
except self.model.DoesNotExist:
raise e
def __unicode__(self):
return str(self.user)
Now I completely admit that I have used the db/models/query.py as my starting point. My problem is this line.
obj = self.model(**params)
I am able to get the params but I haven't defined self.model. I don't understand what that needs to be and it's not intuitively obvious what value that should be. Even looking back at the query.py I can't figure this out. Can someone explain this to me? I would really like to understand it and fix my code.
Thanks
get_or_create is a Manager method, that is you access it via model.objects - it is the manager class that has an attribute model. So maybe the easiest thing to do would be to create a custom Manager and put your method there.
However, fixing your code as it stands is easy. self.model is just the classname - that line is simply instantiating the class with the given parameters. So you could just do
obj = P4User(**params)
although this breaks if you subclass the model.
Daniel was right in his suggestion to use a Manager class. Here is what I ended up with.
# Managers
class P4Manager(models.Manager):
def p4_run_command(self, command):
"""Runs a basic perforce command and return the values"""
p4 = P4.P4()
p4.connect()
values = p4.run(command)
p4.disconnect()
return self.__unify_key_values__(values)
def __unify_key_values__(self, args):
"""Unified method to clean up the lack of standard returns from p4 api"""
final = []
for item in args:
params = dict( [(k.lower(),v) for k, v in item.items()])
results = {}
for k, v in params.items():
if k in ['password', ]: continue
if k in ["access", "update"]:
v = datetime.datetime.strptime(v, "%Y/%m/%d %H:%M:%S")
results[k]=v
final.append(results)
return final
def __get_or_retrieve_singleton__(self, **kwargs):
"""This little sucker will retrieve a key if the server doesn't have it.
In short this will go out to a perforce server and attempt to get a
key if it doesn't exist.
"""
assert len(kwargs.keys())==2, \
'get_or_retrieve() must be passed at one keyword argument'
callback = kwargs.pop('callback', None)
try:
return self.get(**kwargs), False
except self.model.DoesNotExist:
params = self.p4_run_command((kwargs.keys()[0], "-o", kwargs.values()))
if callback:
params = callback(*params)
obj = self.model(**params)
sid = transaction.savepoint()
obj.save(force_insert=True)
transaction.savepoint_commit(sid)
return obj, True
except IntegrityError, e:
transaction.savepoint_rollback(sid)
try:
return self.get(**kwargs), False
except self.model.DoesNotExist:
raise e
class P4UserManager(P4Manager):
"""
A Generic User Manager which adds a retrieve functionality
"""
def get_or_retrieve(self, user):
kwargs = { 'callback' : self.__userProcess__ ,
'user': user }
return self.__get_or_retrieve_singleton__(**kwargs)
def __userProcess__(self, *args):
args = args[0]
if not args.has_key('access'):
raise self.model.DoesNotExist()
return args
# Models
class P4User(models.Model):
"""This simply expands out 'p4 users' """
user = models.CharField(max_length=100, primary_key=True)
fullname = models.CharField(max_length=256)
email = models.EmailField()
access = models.DateField(auto_now_add=True)
update = models.DateField(auto_now_add=True)
objects = P4UserManager()
def __unicode__(self):
return str(self.user)
I hope other find this usefull
Use self instead of self.model.
The code you are copying from, is method for class Queryset. There, self.model is the Model whose queryset is intended to be used. Your method is classmethod of a model itself.