django-tastypie: linking a ModelResource to a Resource - django

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.

Related

How to share validation and schemas between a DRF FilterBackend and a Serializer

I am implementing some APIs using Django Rest Framework, and using the generateschema command to generate the OpenApi 3.0 specs afterwards.
While working on getting the schema to generate correctly, I realized that my code seemed to be duplicating a fair bit of logic between the FilterBackend and Serializer I was using. Both of them were accessing and validating the query parameters from the request.
I like the way of specifying the fields in the Serializer (NotesViewSetGetRequestSerializer in my case), and I would like to use that in my FilterBackend (NoteFilterBackend in my case). It would be nice to have access to the validated_data within the filter, and also be able to use the serializer to implement the filtering schemas.
Are there good solutions out there for only needing to specify your request query params once, and re-using with the filter and serializer?
I've reproduced a simplified version of my code below. I'm happy to provide more info on ResourceURNRelatedField if it's needed (it extends RelatedField and uses URNs instead of primary keys), but I think this would apply to any kind of field.
class NotesViewSet(generics.ListCreateAPIView, mixins.UpdateModelMixin):
allowed_methods = ("GET")
queryset = Note.objects.all()
filter_backends = [NoteFilterBackend]
serializer_class = NotesViewSetResponseSerializer
def get(self, request, *args, **kwargs):
query_params_dict = request.query_params
request_serializer = NotesViewSetGetRequestSerializer(data=query_params_dict)
request_serializer.is_valid(raise_exception=True)
validated_data = request_serializer.validated_data
member = validated_data.get("member_urn")
team = validated_data.get("team_urn")
if not provider_can_view_member(request.user, member, team):
return custom404(
request,
HttpResponseNotFound(
"Member does not exist!. URN={}".format(member.urn())
),
)
return super(NotesViewSet, self).list(request, *args, **kwargs)
class NotesViewSetGetRequestSerializer(serializers.Serializer):
member_urn = ResourceURNRelatedField(queryset=User.objects.all(), required=True)
team_urn = ResourceURNRelatedField(queryset=Team.objects.all(), required=True)
privacy_scope = serializers.CharField(required=False)
def validate_privacy_scope(self, value):
choices = dict(Note.PRIVACY_SCOPE_CHOICES)
if value and value not in choices:
raise serializers.ValidationError(
"bad privacy scope {}. Supported values are: {}".format(value, choices)
)
else:
return value
class NoteFilterBackend(BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
member_urn = request.query_params.get("member_urn")
customer_uuid = URN.from_urn(member_urn).id
privacy_scope = request.query_params.get("privacy_scope")
team_urn = request.query_params.get("team_urn")
team_uuid = URN.from_urn(team_urn).id
queryset = queryset.filter(customer__uuid=customer_uuid)
if privacy_scope == Note.PRIVACY_SCOPE_TEAM_PROVIDERS:
queryset = queryset.filter(team__uuid=team_uuid)
return queryset

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

DRF: accessing a SerializerMethodField during serializer validation

I'm using Django Rest Framework 3.0 and I have a model:
class Vote(models.Model):
name = ...
token = models.CharField(max_length=50)
where token is a unique identifier that I generate from the request IP information to prevent the same user voting twice
I have a serializer:
class VoteSerializer(serializers.ModelSerializer):
name = ...
token = serializers.SerializerMethodField()
class Meta:
model = Vote
fields = ("id", "name", "token")
def validate(self, data):
if Rating.objects.filter(token=data['token'], name=data['name']).exists():
raise serializers.ValidationError("You have already voted for this")
return data
def get_token(self, request):
s = ''.join((self.context['request'].META['REMOTE_ADDR'], self.context['request'].META.get('HTTP_USER_AGENT', '')))
return md5(s).hexdigest()
and a CreateView
But I am getting a
KeyError: 'token'
when I try to post and create a new Vote. Why is the token field not included in the data when validating?
The docs mention:
It can be used to add any sort of data to the serialized representation of your object.
So I would have thought it would be also available during validate?
Investigating, it seems that SerializerMethodField fields are called after validation has occurred (without digging into the code, I don't know why this is - it seems counter intuitive).
I have instead moved the relevant code into the view (which actually makes more sense conceptually to be honest).
To get it working, I needed to do the following:
class VoteCreateView(generics.CreateAPIView):
serializer_class = VoteSerializer
def get_serializer(self, *args, **kwargs):
# kwarg.data is a request MergedDict which is immutable so we have
# to copy the data to a dict first before inserting our token
d = {}
for k, v in kwargs['data'].iteritems():
d[k] = v
d['token'] = self.get_token()
kwargs['data'] = d
return super(RatingCreateView, self).get_serializer(*args, **kwargs)
def get_token(self):
s = ''.join((self.request.META['REMOTE_ADDR'], self.request.META.get('HTTP_USER_AGENT', '')))
return md5(s).hexdigest()
I really hope this isn't the correct way to do this as it seems totally convoluted for what appears to be a pretty straight forward situation. Hopefully someone else can post a better approach to this.

How to deserialize foreignkey with django rest framework?

For example
class People(models.Model):
name = models.CharField(max_length=20)
class Blog(models.Model):
author = models.ForeignKeyField(People)
content = models.TextField()
and then,
class CreateBlogSerializer(serializers.Serializer):
#author id
author = serializers.IntegerField()
content = serializers.TextField()
In views, I need to get author_id , check if the id exists and get the author instance, it's fussy to do that.
serializer = CreateBlogSerializer(data=request.DATA)
if serializer.is_valid():
try:
author = Author.objects.get(pk=serializer.data["author"])
except Author.DoesNotExist:
return Response(data={"author does not exist"})
blog = Blog.objects.create(author=author, content=serializer.data["content"])
Is there a ForeignKeyField to deserialize and validate primarykey data and then return a instance.
class CreateBlogSerializer(serializers.Serializer):
author = serializers.ForeignKeyField(Author)
content = serializers.TextField()
serializer = CreateBlogSerializer(data=request.DATA)
if serializer.is_valid():
#after deserialization , the author id becomes author model instance
blog = Blog.objects.create(author=serializer.data["author"], content=serializer.data["content"])
else:
#the author id does not exist will cause serializer.is_valid=Flase
PS
I knew PrimaryKeyRelatedField in ModelSerializer,but I can't use ModelSerializer here, the model structures are complex, the above are just examples。
My first thought is to write a customer field.
class ForeignKeyField(WritableField):
def __init__(self, model_name, *args, **kwargs):
super(ForeignKeyField, self).__init__(*args, **kwargs)
self.model_name = model_name
self.model_instance = None
def from_native(self, pk):
if not isinstance(pk, int):
raise ValidationError("pk must be int")
try:
self.model_instance = self.model_name.objects.get(pk=pk)
return self.model_instance
except self.model_name.DoesNotExist:
raise ValidationError('object does not exist')
def to_native(self, obj):
return self.model_instance
I hacked it, but I don't konw why it works.
Usage:
there is a little difference
class t_serializer(serializers.Serializer):
author = ForeignKeyField(Author)
#api_view(["POST"])
def func(request):
serializer = t_serializer(data=request.DATA)
if serializer.is_valid():
print isinstance(serializer.data["author"], Author)
#print True
else:
print serializer.errors
It appears that what you want is custom validation code.
For this particular instance, you could write the following.
class CreateBlogSerializer(serializers.Serializer):
author = serializers.ForeignKeyField(Author)
content = serializers.TextField()
def validate_author(self, attrs, source):
"""
Verify that the author exists.
"""
author = attrs[source]
if Author.objects.filter(pk=author).exists():
return attrs
raise serializers.ValidationError("Specified Author does not exist!")
Now when you call serializer.is_valid() this check will occur.
So you can then do this elsewhere in your code,
if serializer.is_valid():
blog = Blog.objects.create(author=serializer.data["author"], content=serializer.data["content"])
And be sure that if the given blog is created, there is a corresponding Author already in the DB.
Detailed Explanation
So Django Rest Framework provides a method of adding custom validation to serializers. This can be done with by providing methods of the following format in the serializer class validate_<field name>. These methods will set the source value to name of the given field, in our example author, which can then be used with the attrs variable to get the current value of the field (attrs contains all the values passed into the serializer).
These validation methods are each called with then serializer.is_valid() method is called. They are supposed to just return attrs if the given test passes, and raise a ValidationError otherwise.
They can get somewhat complex, so it is probably best if you read the full documentation here, http://www.django-rest-framework.org/api-guide/serializers#validation

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.