So here's expected flow of the request:
The user creates a new Language via an html form. [√ ]
Vue via axios will fire a POST request to drf. [√ ]
Drf will validate the data from the POST request (see if the language name/id/etc already exists) [x]
Create the new language if it passes the validation. [x]
I'm currently stuck on #3.
I tried putting this on my LanguageViewSet:
def post(self, request):
language = request.data.get('language')
serializer = LanguageSerializer(data=language)
if serializer.is_valid(raise_exception=True):
language_saved = serializer.save()
return Response({"success": "Language '{}' created successfully!".format(language_saved.name)})
However, this doesn't somewhat work and gets completely ignored since:
I tried commenting the post function, but still if I call a POST request via axios on the LanguageViewSet it would still post. probably a built-in POST feature?
If the function is there, notice I used language = request.data.get('language') which means on my axios, the name of my data to be sent should be language right? otherwise it would ignore the POST request. I used created_lang in axios, fired the POST req but still it posted without any errors as if it completely ignored my post function.
If I tried posting a new language of which it's name is already registered on the database, it would still create it making duplicate records.
Forgive my naiveness I am completely new to drf and django :<
Here's my codes:
Language model:
class Language(models.Model):
name = models.CharField(max_length=100, default='New Language')
def __str__(self):
return self.name
Its serializer:
class LanguageSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Language
fields = ['id', 'name']
view:
class LanguageViewSet(viewsets.ModelViewSet):
queryset = Language.objects.all().order_by('name')
serializer_class = LanguageSerializer
def get_queryset(self):
queryset = Language.objects.all().order_by('name')
lang = self.request.query_params.get('lang','')
if lang:
return Language.objects.filter(pk=lang)
else:
return queryset
and url:
router = routers.DefaultRouter()
router.register(r'languages', views.LanguageViewSet)
On my frontend, here's my form:
<form #submit.prevent="PostLanguage" class="mt-3" action='' method="POST">
<input type="text" v-model="created_lang.name" name="name" id="name" placeholder="Language Name" autocomplete="off" required>
<input type="submit" value="Post">
</form>
And will be posted by this simple Vue script:
PostLanguage(){
let params = Object.assign({}, this.created_lang)
axios.post(
'http://127.0.0.1:8000/api/languages/', params
)
.then(response=>{
console.log(response)
this.GetLanguages()
this.created_lang.name = ''
})
.catch(error => {
console.log(error)
})
}
Update
I tried this:
class LanguageSerializer(serializers.ModelSerializer):
class Meta:
model = Language
fields = ['id', 'name']
def validate_name(self, value):
existed_language = Language.objects.filter(name=value).get()
if existed_language.name == value:
return Response(status=400)
else:
return value
if the name data from the POST is new (it's not used in the database) it would then return the value of it thus creating a new language. However if it already exists, I tried returning a response but it would create a language with its name = <Response status_code=400, "text/html; charset=utf-8">. I know it's kinda funny but I think this is a step to the right direction.
If language is unique in your model then add unique true in your model definition.
class Language(models.Model):
name = models.CharField(max_length=100, default='New Language', unique=True)
def __str__(self):
return self.name
This will cause serializer.validate to fail and won't create duplicate languages.
Update
The validate_name in your serializer returns the value of the filed after executing the validation logic. So you can update it with raise serializers.ValidationError("Language already exist") instead of Response (400) statement.
Related
I'm trying to create a nested comment system using MPTT but using Django Rest Framework to serialize MPTT tree. I got the nested comments to work - and these comments are added, edited, and deleted by calling Django Rest Framework API endpoints only - not using Django ORM DB calls at all. Unfortunately, there is a bug I couldn't figure out! Although the comments are added, edited, and deleted fine - but when a seventh or eighth comment is nested - suddenly the first-in comment or first-in nested comments would become [detail: Not found.] - meaning it will return an empty result or throw an unknown validation error somewhere which I couldn't figure out why. This results in when clicking on edit or delete the buggy comments becoming impossible - but the GET part is fine since these buggy comments do show up in the comment section (or should I say the list part returns fine). The image I'll attach will show that when I entered comment ggggg, the comment aaaa and bbbb will throw errors when trying to edit or delete them. If I delete comment gggg, comment hhhh will also be deleted (as CASCADE was enabled) - and suddenly comment aaaa and bbbb will work again for deletion and editing.
My comment model (models.py):
from django.db import models
from django.template.defaultfilters import truncatechars
from mptt.managers import TreeManager
from post.models import Post
from account.models import Account
from mptt.models import MPTTModel, TreeForeignKey
# Create your models here.
# With MPTT
class CommentManager(TreeManager):
def viewable(self):
queryset = self.get_queryset().filter(level=0)
return queryset
class Comment(MPTTModel):
parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='comment_children')
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comment_post')
user = models.ForeignKey(Account, on_delete=models.CASCADE, related_name='comment_account')
content = models.TextField(max_length=9000)
created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True)
status = models.BooleanField(default=True)
objects = CommentManager()
def __str__(self):
return f'Comment by {str(self.pk)}-{self.user.full_name.__self__}'
#property
def short_content(self):
return truncatechars(self.content, 99)
class MPTTMeta:
# If changing the order - MPTT needs the programmer to go into console and do Comment.objects.rebuild()
order_insertion_by = ['-created_date']
My serializers.py (Showing only comment serializer portion).
class RecursiveField(serializers.Serializer):
def to_representation(self, value):
serializer = self.parent.parent.__class__(value, context=self.context)
return serializer.data
class CommentSerializer(serializers.ModelSerializer):
post_slug = serializers.SerializerMethodField()
user = serializers.StringRelatedField(read_only=True)
user_name = serializers.SerializerMethodField()
user_id = serializers.PrimaryKeyRelatedField(read_only=True)
comment_children = RecursiveField(many=True)
class Meta:
model = Comment
fields = '__all__'
# noinspection PyMethodMayBeStatic
# noinspection PyBroadException
def get_post_slug(self, instance):
try:
slug = instance.post.slug
return slug
except Exception:
pass
# noinspection PyMethodMayBeStatic
# noinspection PyBroadException
def get_user_name(self, instance):
try:
full_name = f'{instance.user.first_name} {instance.user.last_name}'
return full_name
except Exception:
pass
# noinspection PyMethodMayBeStatic
def validate_content(self, value):
if len(value) < COM_MIN_LEN:
raise serializers.ValidationError('The comment is too short.')
elif len(value) > COM_MAX_LEN:
raise serializers.ValidationError('The comment is too long.')
else:
return value
def get_fields(self):
fields = super(CommentSerializer, self).get_fields()
fields['comment_children'] = CommentSerializer(many=True, required=False)
return fields
The API views for comments would look like this:
class CommentAV(mixins.CreateModelMixin, generics.GenericAPIView):
# This class only allows users to create comments but not list all comments. List all comments would
# be too taxing for the server if the website got tons of comments.
queryset = Comment.objects.viewable().filter(status=True)
serializer_class = CommentSerializer
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def perform_create(self, serializer):
# Overriding perform_create. Can create comment using the authenticated account.
# Cannot pretend to be someone else to create comment on his or her behalf.
commenter = self.request.user
now = timezone.now()
before_now = now - timezone.timedelta(seconds=COM_WAIT_TIME)
# Make sure user can only create comment again after waiting for wait_time.
this_user_comments = Comment.objects.filter(user=commenter, created_date__lt=now, created_date__gte=before_now)
if this_user_comments:
raise ValidationError(f'You have to wait for {COM_WAIT_TIME} seconds before you can post another comment.')
elif Comment.objects.filter(user=commenter, level__gt=COMMENT_LEVEL_DEPTH):
raise ValidationError(f'You cannot make another level-deep reply.')
else:
serializer.save(user=commenter)
# By combining perform_create method to filter out only the owner of the comment can edit his or her own
# comment -- and the permission_classes of IsAuthenticated -- allowing only authenticated user to create
# comments. When doing custome permission - such as redefinte BasePermission's has_object_permission,
# it doesn't work with ListCreateAPIView - because has_object_permission is meant to be used on single instance
# such as object detail.
permission_classes = [IsAuthenticated]
class CommentAVAdmin(generics.ListCreateAPIView):
queryset = Comment.objects.viewable()
serializer_class = CommentSerializer
permission_classes = [IsAdminUser]
class CommentDetailAV(generics.RetrieveUpdateDestroyAPIView):
queryset = Comment.objects.viewable().filter(status=True)
serializer_class = CommentSerializer
permission_classes = [CustomAuthenticatedOrReadOnly]
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
if not instance.user.id == self.request.user.id:
return Response({
'Error': 'Comment isn\'t deleted! Please log into the owner account of this comment to delete this comment.'},
status=status.HTTP_400_BAD_REQUEST)
self.perform_destroy(instance)
return Response({'Success': 'Comment deleted!'}, status=status.HTTP_204_NO_CONTENT)
class CommentDetailAVAdmin(generics.RetrieveUpdateDestroyAPIView):
queryset = Comment.objects.viewable()
serializer_class = CommentSerializer
permission_classes = [IsAdminUser]
class CommentDetailChildrenAV(generics.RetrieveUpdateDestroyAPIView):
queryset = Comment.objects.viewable().get_descendants().filter(status=True)
serializer_class = CommentSerializer
permission_classes = [CustomAuthenticatedOrReadOnly]
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
if not instance.user.id == self.request.user.id:
return Response({
'Error': 'Reply isn\'t deleted! Please log into the owner account of this reply to delete this reply.'},
status=status.HTTP_400_BAD_REQUEST)
self.perform_destroy(instance)
return Response({'Success': 'Comment deleted!'}, status=status.HTTP_204_NO_CONTENT)
The API calls would look like this in blog_post app views:
add_comment = requests.post(BLOG_BASE_URL + f'api/post-list/comments/create-comments/',
headers=headers,
data=user_comment)
add_reply = requests.post(BLOG_BASE_URL + f'api/post-list/comments/create-comments/',
headers=headers,
data=user_reply)
requests.request('PUT', BLOG_BASE_URL + f'api/post-list/comments/{pk}/',
headers=headers,
data=user_comment)
response = requests.request('PUT', BLOG_BASE_URL + f'api/post-list/comments/children/{pk}/',
headers=headers,
data=user_comment)
response = requests.request("DELETE", BLOG_BASE_URL + f'api/post-list/comments/{pk}/', headers=headers)
These calls in the blog post app views would allow me to allow authenticated users to create, edit, and delete comments.
Does anyone know why my application got this bug? Any help would be appreciated! I read somewhere about getting a node refresh_from_db() - but how would I do that in the serialization? Also, Comment.objects.rebuild() doesn't help! I also noticed that when I stopped the development server and restarted it, the whole comment tree worked normally again - and I could now edit and delete the non-working comments earlier.
Update:
I also opened up python shell (by doing Python manage.py shell) and tried this for the specific affected comment that when doing API call for edit or delete and got error of Not Found:
from comment.models import Comment
reply = Comment.objects.get(pk=113)
print(reply.content)
I did get the proper output of the comment's content.
Then I also tried to get_ancestors(include_self=True) (using MPTT instance methods) - and I got proper output that when using include_self=True does show the affected comment's node in the output - but calling API endpoint results in Not Found (for GET) still.
I'm super confused now! Why? If I restart the development server by doing Ctrl-C and python manage.py runserver - and revisit the same affected API GET endpoint - this case is comment (child node) with 113 primary key(id) - the endpoint would show proper output and details as if nothing had gone wrong.
Update 2:
Found an interesting Github post: https://github.com/django-mptt/django-mptt/issues/789
This sounds like what I'm experiencing but I'm not using Apache - and this is Django's default development server.
Okie, I figured it out!
I think when calling the same object in the Tree of MPTT for GET and PUT somehow spits out a weird bug that prevents me from editing the affected replies. So, my solution now is just creating an endpoint with API view below:
class CommentChildrenAV(mixins.CreateModelMixin, generics.GenericAPIView):
# This class only allows users to create comments but not list all comments. List all comments would
# be too taxing for the server if the website got tons of comments.
queryset = Comment.objects.viewable().get_descendants().filter(status=True)
serializer_class = CommentSerializer
def get(self, request, pk):
replies = Comment.objects.viewable().get_descendants().filter(status=True, pk=pk)
serializer = CommentSerializer(replies, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def perform_create(self, serializer):
# Overriding perform_create. Can create comment using the authenticated account.
# Cannot pretend to be someone else to create comment on his or her behalf.
commenter = self.request.user
now = timezone.now()
before_now = now - timezone.timedelta(seconds=COM_WAIT_TIME)
# Make sure user can only create comment again after waiting for wait_time.
this_user_comments = Comment.objects.filter(user=commenter, created_date__lt=now, created_date__gte=before_now)
if this_user_comments:
raise ValidationError(f'You have to wait for {COM_WAIT_TIME} seconds before you can post another comment.')
elif Comment.objects.filter(user=commenter, level__gt=COMMENT_LEVEL_DEPTH):
raise ValidationError(f'You cannot make another level-deep reply.')
else:
serializer.save(user=commenter)
# By combining perform_create method to filter out only the owner of the comment can edit his or her own
# comment -- and the permission_classes of IsAuthenticated -- allowing only authenticated user to create
# comments. When doing custome permission - such as redefinte BasePermission's has_object_permission,
# it doesn't work with ListCreateAPIView - because has_object_permission is meant to be used on single instance
# such as object detail.
permission_classes = [IsAuthenticated]
This API view would allow me to pass in the pk of the reply - get JSON response like so:
response = requests.request("GET", BLOG_BASE_URL + f'api/post-list/children/get-child/{pk}/', headers=headers)
Once I have the response in JSON - I could get the original reply content - input this reply content into reply-form's initial data like so:
edit_form = CommentForm(initial=original_comment_data)
Then I'm getting the POST's new content that the user wants to replace the original reply's content with - the gist is the solution I'm now going with is - if the user is authenticated and if the original's JSON content's user_id (meaning the original's commenter of the reply text) is the same as the request.user.id - then I just do:
if request.method == 'POST':
# I can't use API endpoint here to edit reply because some weird bug won't allow me to do so.
# Instead of calling the endpoint api for edit reply - I just update the database with
# using ORM (Object Relational Manager) method.
if request.user.is_authenticated:
print(content[0]['user_id'], os.getcwd())
if request.user.id == content[0]['user_id']:
Comment.objects.filter(status=True, pk=pk).update(content=request.POST.get(strip_invalid_html('content')))
post_slug = content[0]['post_slug']
return redirect('single_post', post_slug)
This now solves my problem for real! I was just hoping that I don't have to cheat by going the route of ORM for editing a reply. I would prefer 100% API calls for all actions in this app. Sigh... but now my app is fully functioning in terms of having a comment system that is nested using MPTT package.
I have 2 Django forms: one, where the user uploads an article, and the second, where the user can edit a list of article words into one of three buckets (change the column value: bucket 1-3).
forms.py
class UploadForm(forms.ModelForm):
class Meta:
model = Upload
fields = ('name','last_name','docfile',)
class Doc_wordsForm(forms.ModelForm):
class Meta:
model= Doc_words
fields= ('id','word','word_type','upload',) #upload is foreign key value
After the user uploads the article, I have a function in views.py that breaks down the uploaded article into a list of words.
I want these words to be looped through and added to a database table(where each row is a word), then have the second form reference these words.
Views.py
# upload_id = (request.GET.get("id"))
if request.method == 'POST':
form = UploadForm(request.POST, request.FILES)
if form.is_valid():
form.save()
data = request.FILES['docfile']#.read().decode('UTF-8')
words=get_keywords(data)
results=list(find_skills(words))
for word in results:
form2 = Resume_words(word = word, word_type='exclude', upload = upload_id)
form2.save()
return render(request, 'word_list.html',{
"results":results
})
else:
form = UploadForm()
return render(request, 'upload.html', {
'form':form
})
I having trouble pulling these pieces together and I'm desperate for help of any kind! I having trouble with the following steps:
I don't know how to capture the current users instance when saving to the table. I get an error in the above Views.py code.
I don't know how to have the second form reference the current user from the first form.
Please let me know if I can provide more information or clarity on anything above. Also, feel free to answer one question, or simply point me to where there is an example similar to this, any light shed is greatly appreciated.
There are many ways to get user's info in view. the most basic way (not recommended, AT ALL!) is to pass user's id to every view from every view. for example in login view you pass user's id in context:
return render(request, 'main_page.html',{
"user_id":user.id
})
and make every view get this id whether in url or query parameter.
using url:
urls.py
path('any/pk/', AnyView.as_view(), name='carrot'),
view.py
class AnyView(Views):
def get(request, pk):
user=User.objects.get(pk=pk)
def post(request, pk):
user=User.objects.get(pk=pk)
your_template.html
<!-- post request -->
<form action="{% url 'carrot' user_id %}" method="post">...</form>
<!-- get request -->
<a href={% url 'carrot' user_id %}></a>
using query parameters:
urls.py
path('any/', AnyView.as_view(), name='carrot'),
view.py
class AnyView(Views):
def get(request):
user=request.GET.get('pk', False)
if user:
user=User.objects.get(pk=pk)
def post(request):
user=request.POST.get('pk', False)
if user:
user=User.objects.get(pk=pk)
your_template.html
<!-- post request -->
<form action="{% url 'carrot' %}?pk={{ user_id }}" method="post">...</form>
<!-- get request -->
a much much better way is using django default authentication for log in, log out, permission handling and finally getting user information from request without all this unnecessary code.
view.py
class AnyView(Views):
def get(request):
user=request.user
def post(request):
user=request.user
to implement django authentication check this link:
https://developer.mozilla.org/en-US/docs/Learn/Server-side/Django/Authentication
I'm working on a social network. I want to load the comments of each post so I make an API call to the server to fetch all the comments of the required posts. The code will make everything clear:
urls.py
path("comments/<int:post_id>", views.load_comments)
models.py
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
commented_by = models.ForeignKey(User, on_delete=models.CASCADE)
comment = models.CharField(max_length=128)
views.py
def load_comments(request, post_id):
"""Returns the comments to index.js"""
try:
# Filter comments returned based on post id
post = Post.objects.get(pk=post_id)
comments = list(Comment.objects.filter(post=post).values())
return JsonResponse({"comments": comments})
except:
return JsonResponse({"error": "Post not found", "status": 404})
index.js
fetch(`/comments/${post_id}`)
.then(res => res.json())
.then(res => {
// Appoints the number of comments in the Modal title
document.querySelector("#exampleModalLongTitle").textContent = `${res.comments.length} Comments`;
res.comments.forEach(comment => {
modal_body = document.querySelector(".modal-body")
b = document.createElement("b");
span = document.createElement("span");
br = document.createElement("br");
span.classList.add("gray")
b.textContent = comment.commented_by_id + " ";
span.textContent = comment.comment;
modal_body.appendChild(b);
modal_body.appendChild(span);
modal_body.appendChild(br);
})
I can get the comment value using comment.comment in js. However, the problem arises when I convert the comments objects to list.values so now I lose the ability to get the user who posted the comment (commented_by)
Any help to get the comment and commented_by values is appreciated.
I also tried in views.py:
def load_comments(request, post_id):
"""Returns the comments to index.js"""
try:
# Filter comments returned based on post id
post = Post.objects.get(pk=post_id)
post_comments = Comment.objects.filter(post=post)
comments = []
for comment in post_comments:
comments.append({
"comment": comment.comment,
"commented_by": commented_by,
})
comments = serializers.serialize('json', comments)
return HttpResponse(comments, content_type='application/json')
except:
return JsonResponse({"error": "Post not found", "status": 404})
However when I try this, it outputs the error.
In Django, the values() function for a ForeignKey field by default returns the id of the related object with the key formed by appending the name of the field with "_id". So in your case, you have your user ID under the key commented_by_id.
If by "loosing the ability to get the user who posted the comment" you mean other user info, like username, then you can pass the fields that you need to the values() function.
comments = list(Comment.objects
.filter(post=post)
.values("comment",
"commented_by__username"
))
will give a dict with the comment text and the user name (assuming that you have a username field in your User model.
1- you should use django_Rest_framework cuz it's very easy to work with api and show related field
2- use Post.objects.get(id=post_id) instead of pk
or you should skip this step by filter the comments depend on post id directly likeComment.objects.all().filter(post_id=post_id)
I'm implementing a website and I need to save in database some variables that comes from HTML. The idea is save the paragraphs of the text that the user marked and save on database to show it when the user access the page (like in medium.com).
When the user click on paragraph I can't refresh the page, I just need to save on database the paragraph id (data-artigo) that was clicked.
That's my view details.html and I need to save in the database the values artigo.pk and lei.pk
<!-- Begin Post Content -->
<div class="article-post">
{% for artigo in artigos %}
<p class='artigo' data-artigo = "{{artigo.pk}}" data-lei = "{{lei.pk}}">
{{artigo}}
</p>
{% endfor %}
</div>
<!-- End Post Content -->
I have a js function that receive those values and set a yellow background (.highlight) to mark the paragraph that was clicked. So I have to save those data in database:
$("p.artigo").on("dblclick",(function(e){
let artigo = $(this).data('artigo');
let lei = $(this).data('lei');
let is_marked;
if ($(this).hasClass( "highlight" )){
$(this).removeClass("highlight");
is_marked = false;
}else{
$(this).addClass("highlight");
is_marked = true;
}
}));
That is the table (my model) when I need to store those data:
class Marcacao(models.Model):
lei = models.ForeignKey(Lei, on_delete=models.CASCADE, verbose_name='Lei', related_name='marcacaoArtigos')
artigo = models.ForeignKey(Lei, on_delete=models.CASCADE, verbose_name='Artigo', related_name='marcacaoLei')
usuario = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='marcacaoUsuário', related_name='marcacaoUsuario')
is_marked = models.BooleanField('Está Marcado?', blank=True, default=False)
description = models.TextField('Descrição', blank = True, null=True)
If I'm not wrong, I think that I need to create a function in the view and pass those data as parameter to this function, but I don't know how to do it.
Your best bet with this depends on how you want this to work.
If the user clicking a paragraph is considered a 'submit' perhaps run an Ajax query from the JS to the view and POST the data back to be put into the model.
However if you want to continue activity on the page consider using Django Rest Framework and creating an API call you can again send off data in an Ajax query to.
My suggestion would be DRF, it's pretty simple to serialize data and save into the model.
https://www.django-rest-framework.org/
You register your api in urls.py:
router = routers.DefaultRouter()
api_patterns = [
router.register(r'selected-paragraph', api.selectedParagraphViewSet)
]
urlpatterns = [
url(r'api/', include(api_patterns)),
Then in your api.py:
class selectedParagraphViewSet(viewsets.ModelViewset):
queryset = Marcacao.objects.all()
serializer_class = MarcacaoSerializer
def create(self, request, *args, **kwargs):
try:
data = {
'lei': request.data.pop('lei'),
'artigo': request.data.pop('artigo'),
'is_marked': request.data.pop('is_marked'),
'usuario': request.user
}
serializer = self.get_serializer(data=data, method='post')
serializer.is_valid(raise_exception=True)
return Response(serializer.data)
except serializers.ValidationError:
print(traceback.print_exc())
raise
Then in your serializers.py:
class MarcacaoSerializer(serializers.ModelSerializer):
def Meta:
model = Marcacao
fields = '__ALL__'
And finally a nice ajax call to send it all off:
var submitData = {
'let': lei,
'artigo': artigo,
'is_marked': is_marked
}
$.ajax({
url: '/api/selected-paragraph',
method: 'PUT',
data: submitData,
dataType: 'json',
crossDomain: true,
xhrFields: {
withCredentials: true
},
success: function(data, stat, xhr) {
console.log("Was a success");
},
failure: function(xhr, stat, err){
console.log('POST error');
console.log((xhr.responseJSON && xhr.responseJSON.msg) ? xhr.responseJSON.msg : '"'+err+'" response when communicating with server.');
}
});
Also good to note is you can eliminate even the need for the create function in api.py if you can pass the request.user in the Ajax call, but I figured this way would show better how the data is input into the database.
I'm trying to implement partial_update with Django Rest Framework but I need some clarification because I'm stuck.
Why do we need to specify partial=True?
In my understanding, we could easily update Demo object inside of partial_update method. What is the purpose of this?
What is inside of serialized variable?
What is inside of serialized variable in partial_update method? Is that a Demo object? What function is called behind the scenes?
How would one finish the implementation here?
Viewset
class DemoViewSet(viewsets.ModelViewSet):
serializer_class = DemoSerializer
def partial_update(self, request, pk=None):
serialized = DemoSerializer(request.user, data=request.data, partial=True)
return Response(status=status.HTTP_202_ACCEPTED)
Serializer
class DemoSerializer(serializers.ModelSerializer):
class Meta:
model = Demo
fields = '__all__'
def update(self, instance, validated_data):
print 'this - here'
demo = Demo.objects.get(pk=instance.id)
Demo.objects.filter(pk=instance.id)\
.update(**validated_data)
return demo
I when digging into the source code of rest_framework and got the following findings:
For question 1. Why do we need to specify partial=True?
This question is related to HTTP verbs.
PUT: The PUT method replaces all current representations of the target resource with the request payload.
PATCH: The PATCH method is used to apply partial modifications to a resource.
Generally speaking, partial is used to check whether the fields in the model is needed to do field validation when client submitting data to the view.
For example, we have a Book model like this, pls note both of the name and author_name fields are mandatory (not null & not blank).
class Book(models.Model):
name = models.CharField('name of the book', max_length=100)
author_name = models.CharField('the name of the author', max_length=50)
# Create a new instance for testing
Book.objects.create(name='Python in a nut shell', author_name='Alex Martelli')
For some scenarios, we may only need to update part of the fields in the model, e.g., we only need to update name field in the Book. So for this case, client will only submit the name field with new value to the view. The data submit from the client may look like this:
{"pk": 1, name: "PYTHON IN A NUT SHELL"}
But you may have notice that our model definition does not allow author_name to be blank. So we have to use partial_update instead of update. So the rest framework will not perform field validation check for the fields which is missing in the request data.
For testing purpose, you can create two views for both update and partial_update, and you will get more understanding what I just said.
Example:
views.py
from rest_framework.generics import GenericAPIView
from rest_framework.mixins import UpdateModelMixin
from rest_framework.viewsets import ModelViewSet
from rest_framework import serializers
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
class BookUpdateView(GenericAPIView, UpdateModelMixin):
'''
Book update API, need to submit both `name` and `author_name` fields
At the same time, or django will prevent to do update for field missing
'''
queryset = Book.objects.all()
serializer_class = BookSerializer
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
class BookPartialUpdateView(GenericAPIView, UpdateModelMixin):
'''
You just need to provide the field which is to be modified.
'''
queryset = Book.objects.all()
serializer_class = BookSerializer
def put(self, request, *args, **kwargs):
return self.partial_update(request, *args, **kwargs)
urls.py
urlpatterns = patterns('',
url(r'^book/update/(?P<pk>\d+)/$', BookUpdateView.as_view(), name='book_update'),
url(r'^book/update-partial/(?P<pk>\d+)/$', BookPartialUpdateView.as_view(), name='book_partial_update'),
)
Data to submit
{"pk": 1, name: "PYTHON IN A NUT SHELL"}
When you submit the above json to the /book/update/1/, you will got the following error with HTTP_STATUS_CODE=400:
{
"author_name": [
"This field is required."
]
}
But when you submit the above json to /book/update-partial/1/, you will got HTTP_STATUS_CODE=200 with following response,
{
"id": 1,
"name": "PYTHON IN A NUT SHELL",
"author_name": "Alex Martelli"
}
For question 2. What is inside of serialized variable?
serialized is a object wrapping the model instance as a serialisable object. and you can use this serialized to generate a plain JSON string with serialized.data .
For question 3. How would one finish the implementation here?
I think you can answer yourself when you have read the answer above, and you should have known when to use update and when to used partial_update.
If you still have any question, feel free to ask. I just read part of the source code of the rest framework, and may have not understand very deeply for some terms, and please point it out when it is wrong...
For partial update - PATCH http method
For full update - PUT http method
When doing an update with DRF, you are supposed to send request data that includes values for all (required) fields. This is at least the case when the request is via the PUT http method. From what I understand, you want to update one or at least not all model instance fields. In this case make a request with the PATCH http method. Django rest framework (DRF) will take care of it out of the box.
Example (with token auth):
curl -i -X PATCH -d '{"name":"my favorite banana"}' -H "Content-Type: application/json" -H 'Authorization: Token <some token>' http://localhost:8000/bananas/
So simple, just override init method of your serializer like that:
def __init__(self, *args, **kwargs):
kwargs['partial'] = True
super(DemoSerializer, self).__init__(*args, **kwargs)
Just a quick note as it seems that nobody has already pointed this out:
serialized = DemoSerializer(request.user, data=request.data, partial=True)
The first argument of DemoSerializer should be a Demo instance, not a user (at least if you use DRF 3.6.2 like me).
I don't know what you are trying to do, but this is a working example:
def partial_update(self, request, *args, **kwargs):
response_with_updated_instance = super(DemoViewSet, self).partial_update(request, *args, **kwargs)
Demo.objects.my_func(request.user, self.get_object())
return response_with_updated_instance
I do the partial update and then I do other things calling my_func and passing the current user and the demo instance already updated.
Hope this helps.
I had an issue where my multi-attribute/field validation in a rest_framework serializer was working with a POST /resources/ request but failing with a PATCH /resources/ request. It failed in the PATCH case because it was only looking for values in the supplied attrs dict and not falling back to values in self.instance. Adding a method get_attr_or_default to do that fallback seems to have worked:
class EmailSerializer(serializers.ModelSerializer):
def get_attr_or_default(self, attr, attrs, default=''):
"""Return the value of key ``attr`` in the dict ``attrs``; if that is
not present, return the value of the attribute ``attr`` in
``self.instance``; otherwise return ``default``.
"""
return attrs.get(attr, getattr(self.instance, attr, ''))
def validate(self, attrs):
"""Ensure that either a) there is a body or b) there is a valid template
reference and template context.
"""
existing_body = self.get_attr_or_default('body', attrs).strip()
if existing_body:
return attrs
template = self.get_attr_or_default('template', attrs)
templatecontext = self.get_attr_or_default('templatecontext', attrs)
if template and templatecontext:
try:
render_template(template.data, templatecontext)
return attrs
except TemplateRendererException as err:
raise serializers.ValidationError(str(err))
raise serializers.ValidationError(NO_BODY_OR_TEMPLATE_ERROR_MSG)
I don't know why, but for me, the only way to solve it was to override the validate method in the Serializer class.
Maybe it's related to the fact that I'm using MongoDB with Djongo
class DemoSerializer(serializers.ModelSerializer):
def validate(self, attrs):
self._kwargs["partial"] = True
return super().validate(attrs)
You forgot serializer.save()
You can finish it the following way . . .
class DemoViewSet(viewsets.ModelViewSet):
serializer_class = DemoSerializer
def partial_update(self, request, pk=None):
serializer = DemoSerializer(request.user, data=request.data, partial=True)
serializer.save()
serializer.is_valid(raise_exception=True)
return Response(serializer.data)
Also, you shouldn't need to override the update method in the serializer.