Django REST endpoint for list of objects - django

I have a Django application which under /api/v1/crm/ticket can create tickets via a POST call. Now I want to be able to send different types of tickets (more then the one in the example code) to the same endpoint having a "dynamic" serializer depending on the data send. The endpoint should select the right "model" depending on the data properties existing in the request data.
I tried Django db.models but did not get them to work as I write the tickets to another external system and just pass them through, so no database table is existing and the model lacks the necessary primary key.
Can you help me out how to add more ticket types having the same endpoint?
Code
class TicketAPIView(CreateAPIView):
serializer_class = TicketSerializer
permission_classes = (IsAuthenticated,)
class TicketSerializer(serializers.Serializer):
title = serializers.CharField(max_length=256)
description = serializers.CharField(max_length=2048)
type = serializers.ChoiceField(TICKET_TYPES)
def create(self, validated_data):
if validated_data['type'] == 'normal':
ticket = TicketPOJO(
validated_data['title'],
validated_data['description'],
)
...
else:
raise Exception('Ticket type not supported')
return ticket
Files
/my-cool-app
/apps
/crm
/api
/v1
/serializers
serializers.py
__init.py
urls.py
views.py
/clients
/ticket
provider.py
/user
provider.py
/search
/config

Since your models are different for each of your ticket type, I would suggest you create an individual serializer that validates them for each different model with one generic view.
You can override the get_serializer method in your view to select an appropriate serializer depending upon the type of ticket. Something like this
def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
type = self.request.data.get("type", '')
if type === 'normal':
return NormalTicketSerializer(*args, **kwargs)
elif type == 'abnormal':
return AbnormalTicketSerializer(*args, **kwargs)
else:
raise ParseError(detail='Ticket type not supported') # This will return bad request response with status code 400.
Hope this helps.

Related

How to not set image field as null when PUT/Patch in Django rest framework

I have a website that has users each user has his own profile, the problem is when the user wants for example edit his email or username and save this appear profile_image: The submitted data was not a file. Check the encoding type on the form. I thought that the frontend problem and I tried to edit the user username field without touch the image field in the Django rest framework API screen but it shows the same problem the user image field has a path to his image in getting but in put the image input is empty, how I can get edit the user other fields without loss the user image
my view
class UserProfileRetrieveUpdate(generics.GenericAPIView):
serializer_class = UserProfileChangeSerializer
def get(self, request, *args, **kwargs):
serializer = UserProfileChangeSerializer(
instance=request.user)
return Response(serializer.data)
def put(self, request, *args, **kwargs):
user_profile_serializer = UserProfileChangeSerializer(
instance=request.user,
data=request.data,
)
if user_profile_serializer.is_valid():
user_profile_serializer.save()
print(user_profile_serializer.data)
return Response(user_profile_serializer.data, status=status.HTTP_200_OK)
errors = dict()
errors.update(user_profile_serializer.errors)
return Response(errors, status=status.HTTP_400_BAD_REQUEST)
My serializer
class UserProfileChangeSerializer(serializers.ModelSerializer):
profile_image = serializers.ImageField()
class Meta:
model = Account
fields = ('pk', 'email', 'username', 'UserFullName', 'bio', 'profile_image')
There are multiple ways to approach this problem.
A. Add a PATCH request to the class UserProfileRetrieveUpdate in your view.
The slight difference between the PUT and PATCH request can solve the problem.
The difference between PUT and PATCH request.
With PUT request, the client is requesting a full update to the resource and hence must send all the values for the attributes of a model class. Thus, the new instruction replaces the previous one completely. Missing out on certain key value pairs in the request, would lead to setting the default values assigned in the model class.
With PATCH request, the client is requesting a partial update to the resource and hence shall send only the values for the attributes of a model class that need updating. Thus, the new instruction updates the previous instruction with the received key value pairs in the request.
Hence, if we use the PATCH request, then there would be no need to send the profile_image data and still it will remain preserved.
Suggested Code
A new method patch needs to be added to the class UserProfileRetrieveUpdate in your view, and the user_profile_serializer shall be assigned with attribute partial=True to the UserProfileChangeSerializer as it indicates that there will be partial updating of data instead of the default, which is full updating.
def patch(self, request, *args, **kwargs):
user_profile_serializer = UserProfileChangeSerializer(
instance=request.user,
data=request.data,
partial=True
)
if user_profile_serializer.is_valid():
user_profile_serializer.save()
return Response(user_profile_serializer.data, status=status.HTTP_200_OK)
return Response(user_profile_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
B. Modify models.py
This section entails additional changes that can help improve the code.
To accommodate these solutions, edit the class UserProfile in the models.py file.
1. Keep in mind
DON'T ever set ImageField with null=True as Django stores the path from MEDIA_ROOT to file/image in a CharField.
2. Setting imagefield as optional
If the ImageField can be optional, then set the attribute blank=True as it will allow empty values in the database.
profile_image = models.ImageField(upload_to='image_directory', blank=True)
Please Note: image_directory is the path directly in MEDIA_ROOT.
3. Setting a default image (only beneficial for POST request)
If there is a default image that can be set when no image path is passed to the ImageField, then set the attribute default=default.jpg. This is beneficial for POST requests with no value passed to the profile_image as it will set its path to default.jpg.
profile_image = models.ImageField(upload_to='image_directory', default=default.jpg)
Please Note: default.jpg path must exist in the MEDIA_ROOT.
I Solved this qustion by 1- remove the profile_image = serializers.ImageField() from the serializer 2- I added this function to the UserProfileChangeSerializer this can get the user profile image url
def get_profile_image_url(self, obj):
return obj.profile_image.url

Generate schema for Django rest framework viewset actions

As per the DRF documentation I started using ViewSet and have implemented list, retrieve, create, update and destroyactions. I have another APIView for which I was able to write schema (ManualSchema) and when I navigate to /docs/ I am able to the documentation as well as live endpoint for interaction.
I wish to create separate schema for each of the viewset action. I tried writing one but it doesn't show up so I think I am missing something.
Here is the code:
class Clients(viewsets.ViewSet):
'''
Clients is DRF viewset which implements `create`, `update`, `read` actions by implementing create, update, list and retrieve functions respectively.
'''
list_schema = schemas.ManualSchema(fields=[
coreapi.Field(
'status',
required=False,
location='query',
description='Accepted values are `active`, `inactive`'
),
],
description='Clients list',
encoding='application/x-www-form-urlencoded')
#action(detail=True, schema=list_schema)
def list(self, request):
'''Logic for listing'''
def retrieve(self, request, oid=None):
'''Logic for retrieval'''
create_schema = schemas.ManualSchema(fields=[
coreapi.Field(
'name',
required=False,
location='body',
),
coreapi.Field(
'location',
required=False,
location='body',
),
],
description='Clients list',
encoding='application/x-www-form-urlencoded')
#action(detail=True, schema=create_schema)
def create(self, request):
'''Logic for creation'''
So I will answer my own question. I took a look at DRF source code for schema generation. I came up with the plan and performed following steps.
I subclassed SchemaGenerator class defined in rest_framework.schemas module. Below is the code.
class CoreAPISchemaGenerator(SchemaGenerator):
def get_links(self, request=None, **kwargs):
links = LinkNode()
paths = list()
view_endpoints = list()
for path, method, callback in self.endpoints:
view = self.create_view(callback, method, request)
path = self.coerce_path(path, method, view)
paths.append(path)
view_endpoints.append((path, method, view))
if not paths:
return None
prefix = self.determine_path_prefix(paths)
for path, method, view in view_endpoints:
if not self.has_view_permissions(path, method, view):
continue
actions = getattr(view, 'actions', None)
schemas = getattr(view, 'schemas', None)
if not schemas:
link = view.schema.get_link(path, method, base_url=self.url)
subpath = path[len(prefix):]
keys = self.get_keys(subpath, method, view, view.schema)
insert_into(links, keys, link)
else:
action_map = getattr(view, 'action_map', None)
method_name = action_map.get(method.lower())
schema = schemas.get(method_name)
link = schema.get_link(path, method, base_url=self.url)
subpath = path[len(prefix):]
keys = self.get_keys(subpath, method, view, schema)
insert_into(links, keys, link)
return links
def get_keys(self, subpath, method, view, schema=None):
if schema and hasattr(schema, 'endpoint_name'):
return [schema.endpoint_name]
else:
if hasattr(view, 'action'):
action = view.action
else:
if is_list_view(subpath, method, view):
action = 'list'
else:
action = self.default_mapping[method.lower()]
named_path_components = [
component for component
in subpath.strip('/').split('/')
if '{' not in component
]
if is_custom_action(action):
if len(view.action_map) > 1:
action = self.default_mapping[method.lower()]
if action in self.coerce_method_names:
action = self.coerce_method_names[action]
return named_path_components + [action]
else:
return named_path_components[:-1] + [action]
if action in self.coerce_method_names:
action = self.coerce_method_names[action]
return named_path_components + [action]
I specifically modified two functions get_links and get_keys as that allow me to achieve what I wanted.
Further, for all the functions in viewsets that I was writing I dedicated an individual schema for it. I simply created a dictionary to keep mappings of function name to schema instance. For better approach I created a separate file to store schemas. For eg. if I had a viewset Clients I created a corresponding ClientsSchema class and within in defined staticmethods which returned schema instances.
Example,
In file where I am defining my schemas,
class ClientsSchema():
#staticmethod
def list_schema():
schema = schemas.ManualSchema(
fields=[],
description=''
)
schema.endpoint_name = 'Clients Listing'
return schema
In my apis.py,
class Clients(viewsets.ViewSet):
schemas = {
'list': ClientsSchema.list_schema()
}
def list(self, request, **kwargs):
pass
This setup allows me to define schemas for any type of functions that I add to my viewsets. In addition to it, I also wanted that the endpoints have an identifiable name and not the one generated by DRF which is like a > b > update > update.
In order to achieve that, I added endpoint_name property to schema object that is returned. That part is handled in get_keys function that is overridden.
Finally in the urls.py where we include urls for documentation we need to use our custom schema generator. Something like this,
urlpatterns.append(url(r'^livedocs/', include_docs_urls(title='My Services', generator_class=CoreAPISchemaGenerator)))
For security purposes I cannot share any snapshots. Apologies for that. Hope this helps.
I think what you are trying to do is not possible. The ViewSet does not provide any method handlers, hence, you cannot use the #action decorator on the methods create and list, as they are existing routes.

Accessing and Saving model objects directly without using serializers in django rest framework

I have created an api endpoint for one of my resources called "creative" . However I needed to have an action to implement pause and start of this resource (to change the field called status) . Hence I created a #detail_route() for the above actions .
What am I trying to achieve: I need to update a field of this particular resource(model) Creative as well as a field of another model whose foreign key is this model.
I am calling a custom function toggleAdGroupState inside this #detail_route() method where I directly work on the django models rather than using the serializers. Is this allowed ? Is this a proper way of implementing what i need to do ?
class CreativeSerializer(serializers.ModelSerializer):
class Meta:
model = Creative
fields = ('__all__')
#detail_route(methods=['post','get','put'])
def startcreative(self, request, pk=None):
try:
creative_object= self.get_object()
creative_object.active_status=Creative._STATUS_ACTIVE
creative_object.save()
toggleAdGroupState(creative_object.id,"active")
except Exception as e:
print 'error is ',str(e)
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response(status=status.HTTP_200_OK)
def toggleAdGroupState(creative_id,toggleFlag):
adgroup_obj = AdGroup.objects.filter(creative=creative_id)
for item in adgroup_obj:
if(toggleFlag == 'active'):
item.userActivate()
else:
item.userInactivate()
djangorestframework is 3.2.2
I believe are following these steps
user clicks to do post/put request.
this request active_status from
model creative Next you want to update Another Model.
You can do like this in view for each method. I have shown for put,
class View(generics.UpdateAPIView):
....
def update(self, request, *args, **kwargs):
# update creative model
response = generics.UpdateAPIView.update(self, request, *args, **kwargs)
if response.status == 200:
# update_another_model()

Django rest framework migrating from 0.x to 2.1.9

After resolving some of my troubles while converting from django-rest-framwork 0.3.2 to the lates 2.1.9 I cannot see to fix this one (which i agree with a blog of Reinout.... it's a real pain in the ...)
I had this code:
class ApiSomeInputView(View):
form = ApiSomeForm
permissions = (IsAuthenticated, )
resource=SomeResource
def get(self, request):
"""
Handle GET requests.
"""
return "Error: No GET request Possible, use post"
def post(self, request, format=None):
some_thing = self.CONTENT['some_thing']
# check if something exist:
something = get_object_or_none(Something,some_field=int(some_thing))
if not something:
raise _404_SOMETHING_NOT_FOUND
#Note exludes are set in SomeResource
data = Serializer(depth=4).serialize(something)
return Response(status.HTTP_200_OK, data)
Now I have followed the tutorial and saw how you can do this different (maybe even prettier). By using slug in the url.
However.... I want to keep things backward compatible for the client side software... so I want to have this without putting the value of the query in the url. The client side uses json data and ContentType json in the header of a post.
In the first version of django rest framwork, I even got a nice browsable form in which to fill in the values for this query
My question: how to get this done in the latest version?
I can't seem to get a form in the views.... where I can fill in values and use in the proces
maybe good to post what I have tried until sofar...
first I changed the ModelResource in a Serializer:
class SomethingSerializer(HyperlinkedModelSerializer):
class Meta:
model = Something
#exclude = ('id',)
depth = 4
and than the view changed in to:
class ApiSomeInputView(APIView):
permissions = (IsAuthenticated, )
def post(self, request, format=None):
some_thing = request.DATA['some_thing']
# check if something exist: .... well actually this above already does not work
something = get_object_or_none(Something,some_field=int(some_thing))
if not something:
raise _404_SOMETHING_NOT_FOUND
serializer = SomethingSerializer(something)
return Response(status.HTTP_200_OK, serializer.data)
Note: Bases upon the accepted answer (by Tom Christie) I als put an answer in which I show how I got it working (in more detail).
When you're inheriting from APIView, the browseable API renderer has no way of knowing what serializer you want to use to present in the HTML, so it falls back to allowing you to post a plain JSON (or whatever) representation.
If you instead inherit from GenericAPIView, set the serializer using the serializer_class attribute, and get an instance of the serializer using the get_serializer(...) method - see here, then the browseable API will use a form to display the user input.
Based upon the answer of Tom Christie (which I'll accept as the answer). I got it working:
I made an extra serializer which defines the field(s) to be shown to fill in for the post and shown using the GenericAPIView... (correct me if I Am wrong Tom, just documenting it here for others... so better say it correct)
class SomethingSerializerForm(Serializer):
some_thing = serializers.IntegerField()
And with this serializer and the other one I aready had.
And a view:
class ApiSomeInputView(GenericAPIView):
permissions = (IsAuthenticated, )
model = Something
serializer_class = SomethingSerializerForm
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA)
if not serializer.is_valid():
raise ParseError(detail="No valid values")
some_thing = request.DATA['some_thing']
something = get_object_or_none(Something,some_field=int(some_thing))
if not something:
raise Http404
serializer = SomethingSerializer(something)
return Response(serializer.data)
Above is working, and exactly the same as before....
I still got the feeling I Am abusing the Serializer class as a Form.

How to limit fields in django-admin depending on user?

I suppose similar problem would have been discussed here, but I couldn't find it.
Let's suppose I have an Editor and a Supervisor. I want the Editor to be able to add new content (eg. a news post) but before publication it has to be acknowledged by Supervisor.
When Editor lists all items, I want to set some fields on the models (like an 'ack' field) as read-only (so he could know what had been ack'ed and what's still waiting approval) but the Supervisor should be able to change everything (list_editable would be perfect)
What are the possible solutions to this problem?
I think there is a more easy way to do that:
Guest we have the same problem of Blog-Post
blog/models.py:
Class Blog(models.Model):
...
#fields like autor, title, stuff..
...
class Post(models.Model):
...
#fields like blog, title, stuff..
...
approved = models.BooleanField(default=False)
approved_by = models.ForeignKey(User)
class Meta:
permissions = (
("can_approve_post", "Can approve post"),
)
And the magic is in the admin:
blog/admin.py:
...
from django.views.decorators.csrf import csrf_protect
...
def has_approval_permission(request, obj=None):
if request.user.has_perm('blog.can_approve_post'):
return True
return False
Class PostAdmin(admin.ModelAdmin):
#csrf_protect
def changelist_view(self, request, extra_context=None):
if not has_approval_permission(request):
self.list_display = [...] # list of fields to show if user can't approve the post
self.editable = [...]
else:
self.list_display = [...] # list of fields to show if user can approve the post
return super(PostAdmin, self).changelist_view(request, extra_context)
def get_form(self, request, obj=None, **kwargs):
if not has_approval_permission(request, obj):
self.fields = [...] # same thing
else:
self.fields = ['approved']
return super(PostAdmin, self).get_form(request, obj, **kwargs)
In this way you can use the api of custom permission in django, and you can override the methods for save the model or get the queryset if you have to. In the methid has_approval_permission you can define the logic of when the user can or can't to do something.
Starting Django 1.7, you can now use the get_fields hook which makes it so much simpler to implement conditional fields.
class MyModelAdmin(admin.ModelAdmin):
...
def get_fields(self, request, obj=None):
fields = super(MyModelAdmin, self).get_fields(request, obj)
if request.user.is_superuser:
fields += ('approve',)
return fields
I have a system kind of like this on a project that I'm just finishing up. There will be a lot of work to put this together, but here are some of the components that I had to make my system work:
You need a way to define an Editor and a Supervisor. The three ways this could be done are 1.) by having an M2M field that defines the Supervisor [and assuming that everyone else with permission to read/write is an Editor], 2.) make 2 new User models that inherit from User [probably more work than necessary] or 3.) use the django.auth ability to have a UserProfile class. Method #1 is probably the most reasonable.
Once you can identify what type the user is, you need a way to generically enforce the authorization you're looking for. I think the best route here is probably a generic admin model.
Lastly you'll need some type of "parent" model that will hold the permissions for whatever needs to be moderated. For example, if you had a Blog model and BlogPost model (assuming multiple blogs within the same site), then Blog is the parent model (it can hold the permissions of who approves what). However, if you have a single blog and there is no parent model for BlogPost, we'll need some place to store the permissions. I've found the ContentType works out well here.
Here's some ideas in code (untested and more conceptual than actual).
Make a new app called 'moderated' which will hold our generic stuff.
moderated.models.py
class ModeratedModelParent(models.Model):
"""Class to govern rules for a given model"""
content_type = models.OneToOneField(ContentType)
can_approve = models.ManyToManyField(User)
class ModeratedModel(models.Model):
"""Class to implement a model that is moderated by a supervisor"""
is_approved = models.BooleanField(default=False)
def get_parent_instance(self):
"""
If the model already has a parent, override to return the parent's type
For example, for a BlogPost model it could return self.parent_blog
"""
# Get self's ContentType then return ModeratedModelParent for that type
self_content_type = ContentType.objects.get_for_model(self)
try:
return ModeratedModelParent.objects.get(content_type=self_content_type)
except:
# Create it if it doesn't already exist...
return ModeratedModelParent.objects.create(content_type=self_content_type).save()
class Meta:
abstract = True
So now we should have a generic, re-usable bit of code that we can identify the permission for a given model (which we'll identify the model by it's Content Type).
Next, we can implement our policies in the admin, again through a generic model:
moderated.admin.py
class ModeratedModelAdmin(admin.ModelAdmin):
# Save our request object for later
def __call__(self, request, url):
self.request = request
return super(ModeratedModelAdmin, self).__call__(request, url)
# Adjust our 'is_approved' widget based on the parent permissions
def formfield_for_dbfield(self, db_field, **kwargs):
if db_field.name == 'is_approved':
if not self.request.user in self.get_parent_instance().can_approve.all():
kwargs['widget'] = forms.CheckboxInput(attrs={ 'disabled':'disabled' })
# Enforce our "unapproved" policy on saves
def save_model(self, *args, **kwargs):
if not self.request.user in self.get_parent_instance().can_approve.all():
self.is_approved = False
return super(ModeratedModelAdmin, self).save_model(*args, **kwargs)
Once these are setup and working, we can re-use them across many models as I've found once you add structured permissions for something like this, you easily want it for many other things.
Say for instance you have a news model, you would simply need to make it inherit off of the model we just made and you're good.
# in your app's models.py
class NewsItem(ModeratedModel):
title = models.CharField(max_length=200)
text = models.TextField()
# in your app's admin.py
class NewsItemAdmin(ModeratedModelAdmin):
pass
admin.site.register(NewsItem, NewsItemAdmin)
I'm sure I made some code errors and mistakes in there, but hopefully this can give you some ideas to act as a launching pad for whatever you decide to implement.
The last thing you have to do, which I'll leave up to you, is to implement filtering for the is_approved items. (ie. you don't want un-approved items being listed on the news section, right?)
The problem using the approach outlined by #diegueus9 is that the ModelAdmin acts liked a singleton and is not instanced for each request. This means that each request is modifying the same ModelAdmin object that is being accessed by other requests, which isn't ideal. Below is the proposed solutions by #diegueus9:
# For example, get_form() modifies the single PostAdmin's fields on each request
...
class PostAdmin(ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if not has_approval_permission(request, obj):
self.fields = [...] # list of fields to show if user can't approve the post
else:
self.fields = ['approved', ...] # add 'approved' to the list of fields if the user can approve the post
...
An alternative approach would be to pass fields as a keyword arg to the parent's get_form() method like so:
...
from django.contrib.admin.util import flatten_fieldsets
class PostAdmin(ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if has_approval_permission(request, obj):
fields = ['approved']
if self.declared_fieldsets:
fields += flatten_fieldsets(self.declared_fieldsets)
# Update the keyword args as needed to allow the parent to build
# and return the ModelForm instance you require for the user given their perms
kwargs.update({'fields': fields})
return super(PostAdmin, self).get_form(request, obj=None, **kwargs)
...
This way, you are not modifying the PostAdmin singleton on every request; you are simply passing the appropriate keyword args needed to build and return the ModelForm from the parent.
It is probably worth looking at the get_form() method on the base ModelAdmin for more info: https://code.djangoproject.com/browser/django/trunk/django/contrib/admin/options.py#L431