django rest update_or_create - django

Background:
I am trying to create a system which allows users to vote on comments written by other users (similar to Reddit). Users have the choice of three vote values: -1, 0, or 1. I have created a POST API (using django rest-framework) that will store a user's vote with respect to a particular comment. If the user has already voted on the given comment, then it will update the existing user’s vote value to the new one.
I drew inspiration from this post:Django RF update_or_create
Problem:
Once a comment has had one user submit a vote on it, Django creates a duplicate comment object with the same ID/primary key whenever another user votes on the same comment. I have taken a screenshot of my admin page where it says I have 3 objects to select but only a single comment. Why is it doing this and how can I prevent it?
Screenshot of my comment admin page
I am new to Django. I suspect I might be doing something wrong when I define my own "create" method in my serializer. I'd appreciate any help. Thanks!
models.py:
Comment model:
class Comment(models.Model):
location_property_category = models.ForeignKey('locations.LocationPropertyCategory',on_delete=models.CASCADE,related_name='comments',null=True)
author = models.ForeignKey('auth.User',on_delete=models.PROTECT,related_name='comments')
location = models.ForeignKey('locations.Location',on_delete=models.CASCADE,related_name='comments')
text = models.TextField()
create_date = models.DateTimeField(default=timezone.now())
published_date = models.DateTimeField(blank=True,null=True)
approved_comment = models.BooleanField(default=False)
objects = CommentManager()
def approve(self):
self.approved_comment = True
self.save()
def __str__(self):
return self.text
def save(self, *args, **kwargs):
if self.approved_comment is True:
self.published_date = timezone.now()
super(Comment, self).save(*args, **kwargs)
def sum_vote(self):
return self.upvotedownvotes.aggregate(Sum('vote')).get('vote__sum') or 0
Vote model:
class UpVoteDownVote(models.Model):
UPVOTE = 1
NEUTRALVOTE = 0
DOWNVOTE = -1
VOTES = (
(UPVOTE, 'Upvote'),
(NEUTRALVOTE, 'Neutralvote'),
(DOWNVOTE, 'Downvote')
)
vote = models.SmallIntegerField(choices=VOTES)
user = models.ForeignKey('auth.User', related_name='upvotedownvotes', on_delete=models.CASCADE)
comment = models.ForeignKey(Comment, related_name='upvotedownvotes', on_delete=models.CASCADE)
date_voted = models.DateTimeField(default=timezone.now())
class Meta:
unique_together = (('user','comment'),)
Comment manager model:
class CommentManager(models.Manager):
def get_queryset(self):
return super(CommentManager, self).get_queryset().order_by('-upvotedownvotes__vote')
serializers.py
Vote serializer:
class UpVoteDownVoteSerializer(serializers.ModelSerializer):
class Meta:
model = UpVoteDownVote
fields = ('vote','comment')
def get_fields(self):
fields = super(UpVoteDownVoteSerializer, self).get_fields()
fields['comment'].queryset = Comment.objects.filter(approved_comment=True)
return fields
def create(self, validated_data):
votedata, created = UpVoteDownVote.objects.update_or_create(
user=validated_data.get('user', None),
comment=validated_data.get('comment', None),
defaults={'vote': validated_data.get('vote', None),
})
return votedata
views.py
class UpVoteDownVoteCreateApiView(generics.CreateAPIView):
serializer_class = UpVoteDownVoteSerializer
permission_classes = [IsAuthenticated]
def perform_create(self,serializer):
serializer.save(user=self.request.user)
Comment app admin.py
class CommentAdmin(admin.ModelAdmin):
readonly_fields = ('id',)
admin.site.register(Comment,CommentAdmin)

Welcome to StackOverflow!
Your problem is in CommentManager:
queryset.order_by('-upvotedownvotes__vote')
This query basically creates LEFT_OUTER_JOIN. So you result looks like:
comment#1 upvote#1
comment#1 upvote#2
comment#1 upvote#3
That is why you are seeing comment#1 3 times.
I believe that you want to use something like this: https://docs.djangoproject.com/en/2.1/topics/db/aggregation/#order-by

Related

Django Admin formfield_for_foreignkey for manytomany

My Model (simpliefied)
we have tours:
class Tour(models.Model):
name = models.CharField(max_length=100)
strecke = models.ManyToManyField(Strecke, through="Streckenreihenfolge")
A tour has sections:
class Strecke(models.Model):
name = models.CharField(max_length=100)
auftrag = models.ForeignKey("Auftrag", on_delete=models.CASCADE, null=True, blank=True)
And the sections are put in order
class Streckenreihenfolge(models.Model):
strecke = models.ForeignKey(Strecke, on_delete=models.CASCADE)
tour = models.ForeignKey("Tour", on_delete=models.CASCADE)
reihenfolge = models.IntegerField()
In my admin, I want to give some restrictions on which sections (Strecke) to show. I thought about using formfield_for_foreignkey. It gets called, but it doesn't have any impact on the options to select from:
#admin.register(Tour)
class TourAdmin(admin.ModelAdmin):
class StreckenreihenfolgeAdminInline(admin.TabularInline):
model = Streckenreihenfolge
autocomplete_fields = ["strecke"]
ordering = ["reihenfolge"]
extra = 0
def formfield_for_foreignkey(self, db_field, request, **kwargs):
print(db_field.name)
if db_field.name == 'strecke':
kwargs['queryset'] = Strecke.objects.filter(auftrag_id__exact=8)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
inlines = [StreckenreihenfolgeAdminInline, ]
Does formfield_for_foreignkey not work for manytomanyfields?
Update
Found some more infos here: Django Admin - Filter ManyToManyField with through model
Apparently formfield_for_manytomany doesn't work for inline forms.
I have then tried to use get_queryset(), which reduced the queryset, but somehow the autocomplete values are still unfiltered.
Maybe this image here is illustrating more what I'm try to achieve:
Finally found the answer here: https://forum.djangoproject.com/t/django-admin-autocomplete-field-search-customization/7455/5
Which results in the following code:
#admin.register(Strecke)
class StreckeAdmin(admin.ModelAdmin):
search_fields = ["name"]
def get_search_results(self, request, queryset, search_term):
print("get serach results")
queryset = queryset.filter(auftrag_id__exact=8)
qs = super().get_search_results(request, queryset, search_term)
print(qs)
return qs

Django get objects created by users, with those users belonging to a list of users

I have a model for followers. Here is the model:
class Follow(models.Model):
followee = models.ForeignKey(User, on_delete=models.CASCADE, related_name="followee")
follower = models.ForeignKey(User, on_delete=models.CASCADE, related_name="follower")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="created at")
updated_at = models.DateTimeField(auto_now=True, verbose_name="updated at")
class Meta:
unique_together = ["followee", "follower"]
def __str__(self):
return "{} is following {}".format(self.follower, self.followee)
def save(self, *args, **kwargs):
if self.followee == self.follower:
return "You cannot follow yourself"
else:
super().save(*args, **kwargs)
Users create multiple types of objects like posts and questions. I would like to show all the posts and questions of all the users that follow a specific user. Simplified show me all the posts and questions of users that follow me.
I am using a module called drf_multiple_model and here is my view, which I cannot get to work. It gives me the following error that I don't understand:
Cannot use QuerySet for "Follow": Use a QuerySet for "User".
Here is the view I am using:
def get_followers(queryset, request, *args, **kwargs):
id = kwargs['user']
user = User.objects.get(id=id)
followers = Follow.objects.all().filter(followee=user)
return queryset.filter(user__in=followers)
class HomeView(FlatMultipleModelAPIView):
permission_classes = [IsAuthenticated]
def get_querylist(self):
querylist = [
{'queryset':Post.objects.all(), 'serializer_class': UserPostSerializer, 'filter_fn': get_followers, 'label':'post'},
{'queryset':Question.objects.all(), 'serializer_class': QuestionSerializer, 'filter_fn': get_followers, 'label':'question'},
]
return querylist
What am I doing wrong please?
In order to be able to use the __in filter the followers should be an iterable of Users. Try this:
followers = [f.follower for f in Follow.objects.filter(followee=user)]
or
followers = [f.follower for f in user.follower.all()]
You can filter with a JOIN, like:
def get_followers(queryset, request, *args, **kwargs):
return queryset.filter(user__follower__followee_id=kwargs['user'])
This will fetch the items in a single query, and thus avoid first querying the followers and then obtain the items.

Hit counter for Django blog post

In my search to find a way to add hit counter to my blog posts without any third party library I found this answer on StackOverflow.
However as I'm not a Django expert I can't figure out how to use that Mixin with my view.
Here's how my model is defined:
class Post(models.Model):
STATUS_CHOICES = (('draft', 'Draft'), ('published', 'Published'))
title = models.CharField(max_length=250)
slug = models.SlugField(max_length=250, unique_for_date='publish')
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_posts')
body = models.TextField()
publish = models.DateTimeField(default=timezone.now)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
And my views:
def post_list(request):
posts = Post.published.all()
return render(request, 'blog/post/list.html', {'posts': posts})
class PostDetailView(DetailView):
model = Post
context_object_name = 'post'
And here's the mixin provided in that answer:
class BlogPostCounterMixin(object):
def get_context_data(self, **kwargs):
context = super(BlogPostCounterMixin, self).get_context_data(**kwargs)
blog_post_slug = self.kwargs['slug']
if not blog_post_slug in self.request.session:
bp = BlogPost.objects.filter(slug=blog_post_slug).update(counter=+1)
# Insert the slug into the session as the user has seen it
self.request.session[blog_post_slug] = blog_post_slug
return context
I would ask the user provided that answer, but he's inactive for more than 2 years.
Appreciate your help.
you can create a IntegerField called views in your Model
then in your post_detail view you could do this just before return statement
post.views += 1
post.save(update_fields=['views'])
Obviously this solution has the drawback of some views that happen at exactly the same moment being missed.
Edit:
before using a mixin you must first use a class based view instead of a function based view then you can use this mixin and in your case you would want to use a DetailView
https://docs.djangoproject.com/en/3.2/ref/class-based-views/generic-display/#detailview
class BlogPostCounterMixin:
def get_object(self, *args, **kwargs):
# get_object will be available if you use a DetailView
obj = super().get_object(*args, **kwargs):
post_unique_key = 'post_%s' % obj.pk # or whatever unique key is
if not post_unique_key in self.request.session:
obj.views += 1
obj.save(update_fields=['views'])
self.request.session[post_unique_key] = post_unique_key
return obj
and can be used like this
class PostDetailView(BlogPostCounterMixin, DetailView):
model = Post
context_object_name = 'post'

Need help getting correct instance for form_valid in a generic view

I can't work out how to get the correct instance for the form_valid part of my generic view.
I am trying to allow a user to post on their project wall(bit like Facebook). I need the post to be related to an individual project(a user can have more than one project). Should the instance be a pk or the project title? Any example code or help would be very appreciated! I struggle understanding how when you create a new post, it knows which project to associate itself with.
views
class NewPost(CreateView):
model = ProjectPost
form_class = ProjectPostForm
template_name = 'howdidu/new_post.html'
def form_valid(self, form):
newpost = form.save(commit=False)
form.instance.user = self.request.user
newpost.save()
self.object = newpost
return super(NewPost, self).form_valid(form)
def get_success_url(self):
project_username = self.request.user.username
project_slug = self.object.slug
return reverse('user_project', kwargs={'username':project_username, 'slug': project_slug})
models
class UserProject(models.Model):
user = models.ForeignKey(User)
title = models.CharField(max_length=100)
project_overview = models.CharField(max_length=1000)
project_picture = models.ImageField(upload_to='project_images', blank=True)
date_created = models.DateTimeField(auto_now_add=True)
project_views = models.IntegerField(default=0)
project_likes = models.IntegerField(default=0)
project_followers = models.IntegerField(default=0)
slug = models.SlugField(max_length=100, unique=True) #should this be unique or not?
def save(self, *args, **kwargs):
self.slug = slugify(self.title)
super(UserProject, self).save(*args, **kwargs)
def __unicode__(self):
return self.title
class ProjectPost(models.Model):
project = models.ForeignKey(UserProject)
title = models.CharField(max_length=100)
post_overview = models.CharField(max_length=1000)
date_created = models.DateTimeField(auto_now_add=True)
post_views = models.IntegerField(default=0)
post_likes = models.IntegerField(default=0)
forms
#form to add project details
class UserProjectForm(forms.ModelForm):
class Meta:
model = UserProject
fields = ('title', 'project_picture', 'project_overview')
#form to create a post
class ProjectPostForm(forms.ModelForm):
class Meta:
model = ProjectPost
fields = ('title', 'post_overview')
Ok, in that case, I would recommend a URL something like
url(r'^(?P<pk>\d+)/post/add/$', views.NewPostCreateView.as_view(), name='...'),
and then a view like
class NewPost(CreateView):
model = ProjectPost
form_class = ProjectPostForm
template_name = 'howdidu/new_post.html'
def form_valid(self, form):
self.object = form.save(commit=False)
# Find project by using the 'pk' in the URL
project = get_object_or_404(UserProject, pk=self.kwargs['pk'])
# Then just set the project on the newPost and save()
self.object.project = project
self.object.save()
return super(NewPost, self).form_valid(form)
def get_success_url(self):
# Unchanged ...
I see in your code that you were trying to do something with the user but I don't understand why your Post does not have a user field (you may want to add a created_by) and the UserProject should already have a user set.
I am also assuming the user got to the his/her project first, so you know by definition that the project he is adding a post to is his. If that is not the case, then just change the logic to get the UserProject through a regular query. e.g. maybe with `UserProject.objects.get(user = self.request.user) if there is one project per user (again, just as an example).
Anyway, I am making some assumptions here, but hopefully the main question was how to set the project on the newPost and that is answered in my example.

Django: Setting current user on a model to use in InlineModelAdmin

I have some models like that:
class BaseModel(models.Model):
created_by = models.ForeignKey(User, related_name="%(app_label)s_%(class)s_created")
created_date = models.DateTimeField(_('Added date'), auto_now_add=True)
last_updated_by = models.ForeignKey(User, related_name="%(app_label)s_%(class)s_updated")
last_updated_date = models.DateTimeField(_('Last update date'), auto_now=True)
class Meta:
abstract = True
class Image(BaseModel):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
name = models.CharField(_('Item name'), max_length=200, blank=True)
image = models.ImageField(_('Image'), upload_to=get_upload_path)
def save(self, *args, **kwargs):
if self.image and not GALLERY_ORIGINAL_IMAGESIZE == 0:
width, height = GALLERY_ORIGINAL_IMAGESIZE.split('x')
super(Image, self).save(*args, **kwargs)
filename = os.path.join( settings.MEDIA_ROOT, self.image.name )
image = PILImage.open(filename)
image.thumbnail((int(width), int(height)), PILImage.ANTIALIAS)
image.save(filename)
super(Image, self).save(*args, **kwargs)
class Album(BaseModel):
name = models.CharField(_('Album Name'), max_length=200)
description = models.TextField(_('Description'), blank=True)
slug = models.SlugField(_('Slug'), max_length=200, blank=True)
status = models.SmallIntegerField(_('Status'),choices=ALBUM_STATUSES)
images = generic.GenericRelation(Image)
I use BaseModel abstract model for my all models to track save and update logs. I can use ModelAdmin class to set user fields automatically:
class BaseAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
if not change:
obj.created_by = request.user
obj.last_updated_by = request.user
obj.save()
class AlbumAdmin(BaseAdmin):
prepopulated_fields = {"slug": ("name",)}
list_display = ('id','name')
ordering = ('id',)
That works. All BaseAdmin fields are filled automatically. But I want to add Images to Albums by Inline. So, I change my admin.py like that:
from django.contrib.contenttypes import generic
class ImageInline(generic.GenericTabularInline):
model = Image
extra = 1
class AlbumAdmin(BaseAdmin):
prepopulated_fields = {"slug": ("name",)}
list_display = ('id','name')
ordering = ('id',)
inlines = [ImageInline,]
When I save page, I get an error: gallery_image.created_by_id may not be NULL on first super(Image, self).save(*args, **kwargs) row of Image model save method. I know it's because of GenericTabularInline class doesn't have a "save_model" method to override.
So, the question is, how can I override save method and set current user on InlineModelAdmin classes?
I have found a solution on another question: https://stackoverflow.com/a/3569038/198062
So, I changed my BaseAdmin model class like that, and it worked like a charm:
from models import BaseModel
class BaseAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
if not change:
obj.created_by = request.user
obj.last_updated_by = request.user
obj.save()
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
for instance in instances:
if isinstance(instance, BaseModel): #Check if it is the correct type of inline
if not instance.created_by_id:
instance.created_by = request.user
instance.last_updated_by = request.user
instance.save()
Note that, you must extend same abstract class for the ModelAdmin that contains the inlines to use this solution. Or you can add that save_formset method to ModelAdmin that contains the inline specifically.
I wanted the user to be set on all my models no matter where/how they were manipulated. It took me forever to figure it out, but here's how to set it on any model using middleware:
"""Add user created_by and modified_by foreign key refs to any model automatically.
Almost entirely taken from https://github.com/Atomidata/django-audit-log/blob/master/audit_log/middleware.py"""
from django.db.models import signals
from django.utils.functional import curry
class WhodidMiddleware(object):
def process_request(self, request):
if not request.method in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
if hasattr(request, 'user') and request.user.is_authenticated():
user = request.user
else:
user = None
mark_whodid = curry(self.mark_whodid, user)
signals.pre_save.connect(mark_whodid, dispatch_uid = (self.__class__, request,), weak = False)
def process_response(self, request, response):
signals.pre_save.disconnect(dispatch_uid = (self.__class__, request,))
return response
def mark_whodid(self, user, sender, instance, **kwargs):
if instance.has_attr('created_by') and not instance.created_by:
instance.created_by = user
if instance.has_attr('modified_by'):
instance.modified_by = user
In addition to mindlace's answer; when the created_by field happens to have null=True the not instance.created_by gives an error. I use instance.created_by_id is None to avoid this.
(I'd rather have posted this as a comment to the answer, but my current reputation doesn't allow...)