Django Tastypie Override URL with slug - python

I have a similar coce:
def override_urls(self):
return [
url(r"^(?P<resource_name>%s)/(?P<slug>[\w\d_.-]+)/$" % self._meta.resource_name, self.wrap_view('dispatch_detail'), name="api_dispatch_detail"),
]
Which produces an URL like:
/api/v1/nodes/<slug>/
Everything fine except that self.get_resource_uri(bundle) returns /api/v1/nodes/<id>/ and I cannot compare the current URL with the resource URI effectively.
What am I doing wrong?
Solution: working code
I implemented the proposed solution here:
https://github.com/ninuxorg/nodeshot/blob/refactoring/nodeshot/core/base/resources.py
Any additional feedback for improvement is welcome.

You could override get_resource_uri on your resource to return the correct uri. After all, the correct one is the one with the slug since that is the (first) one captured by your resource.
Update
The right way to do this is actually to skip override_urls and put this on the Resource's Meta:
detail_uri_name = 'slug'
TLDR Background
I dug a bit deeper and it looks like there's a much better place to implement this. The default implementation of get_resource_uri (indirectly) calls self.detail_uri_kwargs and then reverse.
The default implementation of detail_uri_kwargs in ModelResource just looks up self._meta.detail_uri_name (whose name is not intuitive), and grabs that key off the model. detail_uri_name defaults to pk.
If you just provide a different name here, you can skip the override_urls and the get_resource_uri!
Something like this (building on the code linked in comments by the OP):
from tastypie.resources import ModelResource
from tastypie.bundle import Bundle
class BaseSlugResource(ModelResource):
""" Base Model Resource using slug urls """
class Meta:
abstract = True
detail_uri_name = 'slug'
I'm not sure off the top of my head whether resource Metas are inherited (I'm going to guess they're not), so this may not work as a base class. Luckily, the one line required is probably fine to paste into each Resource that needs it.

I'd like to clean it up with a working example even though the #dokkaebi's answer is marked (and partially) correct. The only missing part is you still have to prepend url that will be resolved for listing and such.
from tastypie.resources import ModelResource
from myapp.models import SomeModel
class BaseSlugResource(ModelResource):
""" Base Model Resource using slug urls """
class Meta:
queryset = SomeModel.objects.all()
detail_uri_name = 'slug'
def prepend_urls(self):
return [
url(r'^(?P<resource_name>%s)/(?P<slug>[\w\.-]+)/$' % self._meta.resource_name, self.wrap_view('dispatch_detail'), name='api_dispatch_detail'),
]
This will display the correct resource_uri's for listing as well as resource get. However, you'll most likely loose {schema} resource (i.e. /api/posts/schema/) as it's treated as a slug too.

Related

How to have 2 Types with the same model in Graphene?

I created a model Checkout on my project, with a CheckoutType to handle the requests, but now i need a Profile, that is basically just getting many of the fields on Checkout. The problem is that Checkout and Profile will be retrieved by users with very different permissions, and the while the first one will have the right ones, the second one must not have them. so i went with creating 2 types:
Checkout:
class CheckoutType(ModelType):
class Meta:
model = Checkout
interfaces = [graphene.relay.Node]
connection_class = CountableConnection
permissions = ['app.view_checkout']
filter_fields = {
'zone': ['exact'],
'vehicle__mark': ['exact'],
'status': ['exact']
}
Profile:
class ProfileFilter(django_filters.FilterSet):
class Meta:
model = Checkout
fields = ['zone','status']
#property
def qs(self):
# The query context can be found in self.request.
return super(ProfileFilter, self).qs.filter(salesman=self.request.user)
class ProfileType(ModelType):
class Meta:
model = Checkout
interfaces = [graphene.relay.Node]
connection_class = CountableConnection
filterset_class = ProfileFilter
The thing here is that, the first one shouldn't filter, and just be a regular schema, while the second one should filter by the user that made the request, that and the permissions is the reason i use 2, but as soon as i implemented, all the tests i did for the Checkout Type started to fail, since it seems it tries to use the ProfileType. I searched a little, and it seems that relay only allows a type per model in Django, so this approach doesn't seems possible, but i'm not sure how to overwrite the CheckoutType on another schema, or how to make a second Type with different permissions and different filters. Does someone knows if this is possible?
Just in case someone is on the same boat, i think i found a way to make it work, but with a different approach, i just modified the CheckoutType a little:
class CheckoutType(ModelType):
# Meta
#classmethod
def get_queryset(cls, queryset, info):
if info.context.user.has_perm('app.view_checkout'):
return queryset
return queryset.filter(salesman=info.context.user)
Basically here i remove the permission from the Meta, since i don't want to check that there, and then i overwrite the get_queryset() to check if the user has the perms, if that's the case, then just return the normal query, but if not just filter(And any additional thing you want to do for people without the permission). I'm not sure if there's a better way, but definitely did the job.

Django REST: serialize url to list of objects of a category

Hard facts:
I am using Django 2.0 with python 3.6, if it makes any difference.
What I am trying to achieve is a link to a list of objects that belong to a summary.
I have a ManyToOne relationship in my models.py.
class Summary(models.model):
type=models.CharField
class Object(models.Model):
summary= models.ForeignKey(Summary, on_delete=models.CASCADE)
in urls.py
object_list= views.ObjectListViewSet.as_view({
'get': 'list'
})
urlpatterns = format_suffix_patterns([
url(r'^summary/(?P<pk>[^/.]+)/objects/$', object_list, name='summary-objects')
])
and now the idea was to give a user the possibility to click the an url in the browsable API and getting all objects.
So, I tried to write a MethodField in serializers.py. I am not able to get any reasonable URL here, the only solution would be to hardcode it.
class SummarySerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="app:summary-detail")
objects= serializers.SerializerMethodField('get_obj_url')
def get_obj_url(self, obj):
pass
class Meta:
model = Summary
Is this possible?
Is it necessary to write a MethodField?
If yes, how do I get the url I need?
Actually, reverse, as suggested in the comments, does the trick.
The solution is:
def get_obj_url(self, obj):
request = self.context.get('request')
return request.build_absolute_uri(reverse('api-root')) + 'summary/{id}/objects'.format(
id=obj.id)
EDIT:Typo

Django: how does allow_tags and short_description works?

Basic way to do it in the models.py, an example:
class Foo(models.Model):
title = models.CharField(max_length=200)
...
def custom_tag(self):
return ('custom: %s' % self.title)
custom_tag.allow_tags = True
custom_tag.short_description = _("Custom Tag")
or, if inside file of admin.py;
class FooAdmin(admin.ModelAdmin):
list_display = ['title', 'custom_tag', ...]
...
def custom_tag(self, instance):
return ('custom: %s' % instance.title)
custom_tag.allow_tags = True
custom_tag.short_description = _("Custom Tag")
admin.site.register(Foo, FooAdmin)
My question is, how does allow_tags and short_description works? and where I can find the relevant documentation?
I can't found it at the documentation or also at source code
You're looking at the development version of the documentation. If you look at the one for Django 1.10 (scroll down to "Deprecated since version 1.9") you'll see that they're removing the allow_tags option and replacing it with other ways of achieving the same thing. There are plenty of examples on how to use short_description though since it's not deprecated.
If you really want to see the source code, here's the line where it gets the short description. Don't worry about the allow_tags since it's removed in 1.11 - it should now done automatically by marking string as safe using mark_safe().
As a side note, you don't need to add the custom_tag() method in both places. The admin looks in both the model and the admin class for it so one is sufficient. If it's not going to be used outside the admin, I would recommend placing it inside the admin class and avoid making the model more complex.

Django form not calling clean_<fieldname> (in this case clean_email)

I couldn't find an answer to the following question, it took me a couple of hours to find out, hence I'm adding it. I'll add my approach of solving it and the answer.
I'm following a YouTube tutorial from This person. For some reason I'm typing the same code, and I checked every single letter. Yet for some reason my cleaning functions aren't called. It's probably something simple, especially since a related question showed something similar. It's probably a framework thing that I get wrong, but I wouldn't know what it is.
Here is the relevant code.
forms.py (complete copy/paste from his Github)
from django import forms
from .models import SignUp
class ContactForm(forms.Form):
full_name = forms.CharField(required=False)
email = forms.EmailField()
message = forms.CharField()
class SignUpForm(forms.ModelForm):
class Meta:
model = SignUp
fields = ['full_name', 'email']
### exclude = ['full_name']
def clean_email(self):
email = self.cleaned_data.get('email')
email_base, provider = email.split("#")
domain, extension = provider.split('.')
# if not domain == 'USC':
# raise forms.ValidationError("Please make sure you use your USC email.")
if not extension == "edu":
raise forms.ValidationError("Please use a valid .EDU email address")
return email
# Final part is ommited, since it's not relevant.
admin.py (typed over from the tutorial)
from django.contrib import admin
# Register your models here.
from .models import SignUp
from .forms import SignUpForm
class SignUpAdmin(admin.ModelAdmin):
list_display = ['__unicode__', 'timestamp', 'updated']
class Meta:
model = SignUp
form = SignUpForm
admin.site.register(SignUp, SignUpAdmin)
After using print statements for a while and reading questions that seemed similar but eventually didn't solve my problem, I decided to look into the source of Django (idea inspired by the most similar question I could find).
Then, I decided to debug the source, since I wanted to know how Django is treating my customized function (thanks to a tutorial + SO answer). In the source I found that the customized functions were called around return super(EmailField, self).clean(value) (line 585, django/forms/fields.py, Django 1.8). When I was stepping through the code I found the critical line if hasattr(self, 'clean_%s' % name): (line 409, django/forms/forms.py, Django 1.8). I checked for the value name which was "email". Yet, the if-statement evaluated as False ((Pdb) p hasattr(self, 'clean_%s' % name)). I didn't understand why, until I figured out that the function name was not registered ((Pdb) pp dir(self)).
I decided to take a look at the whole source code repository and cross-checked every file and then I found that
class Meta:
model = SignUp
form = SignUpForm
means that form / SignUpForm were nested inside the Meta class. At first, I didn't think much of it but slowly I started to realize that it should be outside the Meta class while staying main class (SignUpAdmin).
So form = SignUpForm should have been idented one tab back. For me, as a Django beginner, it still kind of baffles me, because I thought the Meta class was supposed to encapsulate both types of data (models and forms). Apparently it shouldn't, that's what I got wrong.

Strange ModelViewSet behaviour

I'm trying to serialize a MPTT tree model with DRF.
My code:
class SiteTreeCalc(serializers.Field):
def to_representation(self, value):
return value.exists() # return True if has children, False otherwise
class SiteTreeSerializer(serializers.ModelSerializer):
children = SiteTreeCalc()
class Meta:
model = SiteTree
fields = ('id', 'site', 'children')
depth = 1
class SiteTreeViewSet(viewsets.ModelViewSet):
#queryset = SiteTree.objects.all()
serializer_class = SiteTreeSerializer
def get_queryset(self):
if 'pk' not in self.kwargs:
# return first-level nodes
return SiteTree.objects.filter(level=0)
else:
# return all children of a given node
return SiteTree.objects.filter(parent__id=int(self.kwargs['pk']))
router = routers.DefaultRouter()
router.register(r'rest/sitetree', SiteTreeViewSet, "SiteTreeRoots")
router.register(r'rest/sitetree/(?P<tree_id>\d+)/$', SiteTreeViewSet, "SiteTreeChildren")
I have two issues with this code:
I have declared parameter "tree_id" in router registration. However, get_queryset says that parameter name is pk
The second filter never works (the one that should return children of given parent). DRF returns "detail": "Not found.". If I test that line in debugger, it naturally returns all children of the given parent.
I seem to be doing something wrong, but the code seems so obvious to me that I just can't see it.
Help - as always - very appreciated.
Turns out, I wanted to forget the convenient functionality of DefaultRouter the first chance I got.
The problem was that I wanted to create a ViewSet just like any other writable ViewSet, but this particular one was intended only for retrieving items. At least, that's what I intended. But DRF couldn't know that, so my problem #2 was a result of DRF actually checking that I'm returning ONE item with EXACTLY the same pk as was given in the URL.
A solution that works goes like this (as suggested in the DRF ViewSets documentation):
class SiteTreeViewSet(viewsets.ReadOnlyModelViewSet):
queryset = SiteTree.objects.filter(level=0)
serializer_class = SiteTreeSerializer
#detail_route()
def children(self, request, pk=None):
data = SiteTree.objects.filter(parent__id=int(pk))
data = self.get_serializer(data, many=True)
return Response(data.data)
This solution returns first-level items in default mode and also accepts /{pk}/children to return children of the given pk node. Naturally, default operations will still return just the pk node when provided with a /{pk}/ URL.
Router registration remains only the default one:
router.register(r'rest/sitetree', SiteTreeViewSet)
As for 1. you need to set lookup_url_kwarg (the named argument in the urls) on the viewset so it maps to the tree_id.
Note that routers do define the dynamic url part themselves.
As for 2. it's most of the time sending JOSN POST data with form content type. Ensure you are sending the right content type in your request's header.
Edit:
Daniel has the point for 1. With your current url patterns, there's no way to distinguish a detailed top node and a list for the child node.

Categories

Resources