Django: hiding the primary key (pk) and using DetailView with get_queryset - django

Following up on this thread: django: How do I hash a URL from the database object's primary key?, I would like to hide primary keys from users and use Detailview in my urlconf. I was able to accomplish this hiding using bitwise XOR for the most part (it worked in my view functions), until I got to the part where I had to "unmask" the masked primary key that was sent in the url to my subclass of DetailView.
How do I "unmask" my pk_masked named group before sending it to my DetailViewFilteredOnUser(DetailView) instance? Is there a way to send mask_toggle(pk_unmasked) to my call to DetailViewFilteredOnUser right in the urlconf? In searching for solutions, I found something about pk_url_kward in the Django documentation, but I couldn't get it to work and anyways I don't think that can help me perform operations on the primary key that DetailView operates on.
Here is my masking function:
def mask_toggle(number_to_mask_or_unmask):
return int(number_to_mask_or_unmask) ^ settings.MASKING_KEY
My models are "pkgs" that contain "items":
class Pkg(models.Model):
user = models.ForeignKey(User, editable=False)
tracking_number = models.CharField(max_length=60, unique=True)
class Item(models.Model):
pkg = models.ForeignKey(Pkg)
description = models.CharField(max_length=300)
Here is what is in my urls.py:
class ListViewFilteredOnUser(ListView):
def get_queryset(self):
return Pkg.objects.order_by('-created_at').filter(user=self.request.user)
class DetailViewFilteredOnUser(DetailView):
def get_queryset(self):
qs = super(DetailViewFilteredOnUser, self).get_queryset()
return qs.filter(user=self.request.user)
....
url(r'^(?P<pk_masked>\d+)/$',
login_required(DetailViewFilteredOnUser.as_view( model=Pkg,
template_name='pkgs/detail.html'
)),
name='detail'),
So the problem is that if my named group in my urlconf is "pk", then a masked primary key (because the masked key is what is in the url) is sent to DetailView. If my named group in my urlconf is "pk_masked", then I need to do pk=mask_toggle(pk_masked) somewhere, and I can't figure out where or how to do this. Thanks.

If I understood your problem correctly, you need to override get_object (which makes get_queryset pretty much irrelevant, but you can still use it for clarity). Something like:
class DetailViewFilteredOnUser(DetailView):
model = Pkg
template_name = 'pkgs/detail.html'
def get_queryset(self):
return super(DetailViewFilteredOnUser, self).get_queryset().filter(user=self.request.user)
def get_object(self):
return self.get_queryset().get(pk=mask_toggle(self.kwargs.get("pk_masked"))
(Of course, don't forget to catch exceptions, I left that out for clarity and brevity.)

Related

How to display a query on DetailView in Django?

I want my "CustomDetailView" to display a query(a single "flashcard"). I was able to it by using ListView
CustomListView(ListView):
model = Flashcard
template_name = 'flashcards.html
queryset = Flashcard.objects.all()[:1]
But for DetaiView I'm getting this error
Generic detail view CustomDetailView must be called with either an object pk or a slug in the URLconf.
class CustomDetailView(DetailView):
model = Flashcard
template_name = "flashcards.html"
queryset = Flashcard.objects.all().first()
urls.py
path('', CustomDetailView.as_view(), name='flashcards'),
How to fix this?
remove first from queryset:
class CustomDetailView(DetailView):
model = Flashcard
template_name = "flashcards.html"
queryset = Flashcard.objects.all()
lookup_field = 'pk'
lookup_url_kwarg = 'pk'
enter code here
and add id to url:
path('/<int:pk>/', CustomDetailView.as_view(), name='flashcards'),
You need your pk or slug in
path('', CustomDetailView.as_view(), name='flashcards'),
See an example from the docs:
urlpatterns = [
path('<slug:slug>/', ArticleDetailView.as_view(), name='article-detail'),
]
This is because DetailView is inheriting from SingleObjectMixin, and it fetches objects that way. If you want, jump to definition of this class in your IDE to see its implementation of get_queryset and get_object.
How you call your pk or slug really depends on your model, the Flashcard. If your primary_key field is an Integer, I think you'll be fine writing
path('<int:pk>', CustomDetailView.as_view(), name='flashcards'),
edit: as Pourya Mansouri wrote, you do need to remove queryset attribute in your case too
The slug you can customize with slug_field and similar attributes.
You can define a completely custom object fetching behavior by overriding get_object or get_queryset. Here's a random examples of doing that I found on the internet:
Overriding get_object: https://www.valentinog.com/blog/detail/
Overriding get_queryset: https://www.agiliq.com/blog/2019/01/django-when-and-how-use-detailview/
Maybe should have better communicated. Sorry for my English.
I wanted to return a random query when I clicked on a link a show it on detailView. I was able to it by this. Don't think it's efficient. If anyone has any idea share it.
def get_object(self):
queryset = Flashcard.objects.order_by('?').first()
return queryset

How to implement a simple "like" feature in Django REST Framework?

I'm a beginner building the backend API for a social media clone using DRF. The frontend will be built later and not in Django. I'm currently using Postman to interact with the API.
I'm trying to implement a "like" feature as you would have on Facebook or Instagram. I cannot send the correct data with Postman to update the fields which bear the many-to-many relationship.
Here is some of my code:
models.py
class User(AbstractUser):
liked_haikus = models.ManyToManyField('Haiku', through='Likes')
pass
class Haiku(models.Model):
user = models.ForeignKey(User, related_name='haikus', on_delete=models.CASCADE)
body = models.CharField(max_length=255)
liked_by = models.ManyToManyField('User', through='Likes')
created_at = models.DateTimeField(auto_now_add=True)
class Likes(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
haiku = models.ForeignKey(Haiku, on_delete=models.CASCADE)
serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['username', 'password', 'url', 'liked_haikus']
extra_kwargs = { 'password' : {'write_only': True}}
def create(self, validated_data):
password = validated_data.pop('password')
user = User(**validated_data)
user.set_password(password)
user.save()
token = Token.objects.create(user=user)
return user
class HaikuSerializer(serializers.ModelSerializer):
class Meta:
model = Haiku
fields = ['user', 'body', 'liked_by', 'created_at']
class LikesSerializer(serializers.ModelSerializer):
model = Likes
fields = ['haiku_id', 'user_id']
views.py
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]
#action(detail=True, methods=['get'])
def haikus(self, request, pk=None):
user = self.get_object()
serializer = serializers.HaikuSerializer(user.haikus.all(), many=True)
return Response(serializer.data)
class UserCreateViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [permissions.AllowAny]
class HaikuViewSet(viewsets.ModelViewSet):
queryset = Haiku.objects.all()
serializer_class = HaikuSerializer
permission_classes = [permissions.IsAuthenticated]
class LikesViewSet(viewsets.ModelViewSet):
queryset = Likes.objects.all()
serializer_class = LikesSerializer
permission_classes = [permissions.IsAuthenticated]
urls.py
router = routers.DefaultRouter(trailing_slash=False)
router.register('users', views.UserViewSet)
router.register('haikus', views.HaikuViewSet)
router.register('register', views.UserCreateViewSet)
router.register('likes', views.LikesViewSet)
urlpatterns = [
path('admin/', admin.site.urls),
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
path('api-auth-token', obtain_auth_token, name='api_token_auth')
]
Using the Django Admin I can manually set users to like posts and the fields in the db will update and reflect in API requests.
With Postman, I've tried sending both PUT and PATCH to, for example:
http://127.0.0.1:8000/haikus/2
with "form data" where key ="liked_by" and value="3" (Where 3 is a user_id). I got a 200 response and JSON data for the endpoint back, but there was no change in the data.
I've tried GET and POST to http://127.0.0.1:8000/likes and I receive the following error message:
AttributeError: 'list' object has no attribute 'values'
I've looked at nested-serializers in the DRF docs, but they don't seem to be quite the same use-case.
How can I correct my code and use Postman to properly update the many-to-many fields?
I think I need to probably write an update function to one or several of the ViewSets or Serializers, but I don't know which one and don't quite know how to go about it.
All guidance, corrections and resources appreciated.
To update the liked_by Many2Many field, the serializer expect you to provide primary key(s).
Just edit your HaikuSerializer like the following. It will work.
class HaikuSerializer(serializers.ModelSerializer):
liked_by = serializers.PrimaryKeyRelatedField(
many=True,
queryset=User.objects.all())
class Meta:
model = models.Haiku
fields = ['created_by', 'body', 'liked_by', 'created_at']
def update(self, instance, validated_data):
liked_by = validated_data.pop('liked_by')
for i in liked_by:
instance.liked_by.add(i)
instance.save()
return instance
adnan kaya has provided the correct code and I have upvoted him and checked him off as the correct answer. I want go through his solution to explain it for future readers of this question.
liked_by = serializers.PrimaryKeyRelatedField(
many=True,
queryset=User.objects.all())
You can read about PrimaryKeyRelatedField here: https://www.django-rest-framework.org/api-guide/relations/
Since liked_by is a ManyToManyField it has special properties in that ManyToMany relations create a new table in the DB that relates pks to each other. This line tells Django that this field is going to refer to one of these tables via its primary key. It tells it that liked by is going to have multiple objects in it and it tells it that these objects are going to come from a particular queryset.
def update(self, instance, validated_data):
liked_by = validated_data.pop('liked_by')
for i in liked_by:
instance.liked_by.add(i)
instance.save()
return instance
ModelSerializers is a class that provides its own built in create and update functions that are fairly basic and operate in a straightforward manner. Update, for example, will just update the field. It will take the incoming data and use it to replace the existing data in the field it is directed at.
You can read more about ModelSerializers here: https://www.django-rest-framework.org/api-guide/serializers/#modelserializer
You can overwrite these functions and specify custom functions by declaring them. I have declared update here. Update is a function that takes 3 arguments. The first is self. You can call this whatever you want, but there is a strong convention to call it self for readability. Essentially this is importing the class the function belongs, into the function so you can utilize all that classes functions and variables. Next is instance. Instance is the data that is currently in the entry you are trying to update. It is a dictionary like object. Finally, there is validated_data. This is the data you are trying to send to the entry to update it. When using form data, for example, to update a database, this will be a dictionary.
liked_by = validated_data.pop('liked_by')
Because validated_data is a dictionary you can use the .pop() method on it. Pop can take the key of the dictionary and "pop it off" leaving you with the value (more formally, .pop('key') will return its 'value'). This is nice because, at least in my case, it is the value that you want added to the entry.
for i in liked_by:
instance.liked_by.add(i)
this is a simple python for-loop. A for loop is here because in my use-case the value of the validated_data dictionary is potentially a list.
The .add() method is a special method that can be used with ManytoMany relationships. You can read about the special methods for ManytoMany relations here: https://docs.djangoproject.com/en/3.1/ref/models/relations/
It does what it advertises. It will add the value you send send to it to data you call it for, instead of replacing that data. In this case it is instance.liked_by (the current contents of the entry).
instance.save()
This saves the new state of the instance.
return instance
returns the new instance, now with the validated data appended to it.
I'm not sure if this is the most ideal, pythonic, or efficient way implementing a like feature to a social media web app, but it is a straightforward way of doing it. This code can be repurposed to add all sorts of many-to-many relationships into your models (friends lists/followers and tags for example).
This is my understanding of what is going on here and I hope it can help make sense of the confusing topic of ManytoMany relationships for clearer.

Can you filter querysets by user in models?

I suppose the proper way to filter objects by user is done in views because you can easily require login and you have access to request.user. But I have a view that invokes the object's classmethod, which can't be combined with a filter because it converts the queryset to a list object- so I'm thinking if there's a way to filter the queryset by user directly in models, then when I invoke the classmethod in views the objects will already be filtered.
Here's my view:
def leads_by_city(request):
# Invoke Lead classmethod to get the data
data = Lead.objects.get_leads_per_city()
return JsonResponse(data, safe=False)
Model with custom Manager:
class Lead(models.Model):
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
source = models.ForeignKey(LeadSource, on_delete=models.CASCADE)
city = models.CharField(max_length=100)
objects = LeadManager()
class LeadManager(models.Manager):
def get_leads_per_city(self):
queryset = self.values('city').annotate(Count('id')).order_by('city')
data = list(queryset.values('city', 'id__count'))
return data
Is there a way to filter by user in the classmethod? Maybe like:
class LeadManager(models.Manager):
def get_leads_per_city(self,user):
queryset = self.filter(user=Lead.user).values('city').annotate(Count('id')).order_by('city')
data = list(queryset.values('city', 'id__count'))
return data
Yes, although your filtering is a bit off, you can simply set .filter(user=user):
class LeadManager(models.Manager):
def get_leads_per_city(self, user):
queryset = self.filter(
user=user
).values('city').annotate(Count('id')).order_by('city')
return list(queryset.values('city', 'id__count'))
The user=user parameter name does not refer to the user parameter of the function. The Django ORM will look for a field with that name, and we pass it the user of the parameter call.
In your view, you can then call this with:
def leads_by_city(request):
# Invoke Lead classmethod to get the data
data = Lead.objects.get_leads_per_city(request.user)
return JsonResponse(data, safe=False)
That being said, you here use Django's QuerySet mechanism to generate dictionaries. That is typically not advisable. You might want to use a Serializer, as is offered by the Django REST Framework [drf-doc].
Furthermore it also is not a good idea to pass a list as a JSON response. There have been JSON exploits with that, you better wrap this in a dictionary.
Since you require the logged in user, it might also be better to add the #login_required decorator [Django-doc] here, which will return a HTTP redirect response, given the user has not been logged in:
from django.contrib.auth.decorators import login_required
#login_required
def leads_by_city(request):
# Invoke Lead classmethod to get the data
data = Lead.objects.get_leads_per_city(request.user)
return JsonResponse({'data': data})

DetailView using two ForeignKey kwargs

I am trying to utilize Django's class-based generic DetailView by querying the table using two keyword arguments passed via the url. I have tried overriding both the get_queryset() and get_object() method to no avail. My models look like this (edited for brevity, but let me know if something important is missing):
# models
class Skill(models.Model):
skill = models.CharField()
class User(AbstractBaseUser):
username = models.CharField()
class UserSkills(models.Model):
skill = models.ForeignKey(Skill)
user = models.ForeignKey(User, to_field='username')
value = models.CharField()
my url for the DetailView looks like this:
url(
regex=r"^(?P<username>[a-zA-Z0-9-]{1,25})/skills/(?P<skill>[a-zA-Z0-9 -._/]+)/$",
view=views.UserSkillDetail.as_view(),
name='userskill_detail',
),
the view:
class UserSkillDetail(DetailView):
template_name = 'UserSkill_detail.html'
context_object_name = 'skill'
model = UserSkills
def get_object(self, queryset=None):
get_user = self.kwargs['username']
get_skill = self.kwargs['skill']
return get_object_or_404(UserSkills, user__username=get_user, skill__skill=get_skill)
I keep receiving the following error via the template debug message:
No UserSkills matches the given query.
although I am able to successfully query the following via the shell:
>>> x = UserSkills.objects.get(user__username='user1', skill__skill='skill1')
>>> x
<UserSkills: user1#example.com: skill1>
and have verified that the keyword arguments are being captured correctly ('user1', 'skill1') via the logger and Django debug toolbar. Any help would be greatly appreciated!
There was an issue with my migration sequence (called out of intended order). I reconfigured the migrations and everything worked.

Override default queryset in Django admin

One of my models has a deleted flag, which is used to hide objects globally:
class NondeletedManager(models.Manager):
"""Returns only objects which haven't been deleted"""
def get_query_set(self):
return super(NondeletedManager, self).get_query_set().exclude(deleted=True)
class Conversation(BaseModel):
...
deleted = models.BooleanField(default=False)
objects = NondeletedManager()
all_conversations = models.Manager() # includes deleted conversations
How can I override the default queryset used by Django admin module to include deleted conversations?
You can override get_queryset method in your model admin class.
class MyModelAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(author=request.user)
Note in Django<=1.5 the method was named just queryset.
Konrad is correct, but this is more difficult than the example given in the documentation.
Deleted conversations can't be included in a queryset that already excludes them. So I don't see an option other than re-implementing admin.ModelAdmin.queryset entirely.
class ConversationAdmin (admin.ModelAdmin):
def queryset (self, request):
qs = Conversation.all_conversations
ordering = self.get_ordering(request)
if ordering:
qs = qs.order_by(*ordering)
return qs
You can do this with a Django proxy model.
# models.py
class UnfilteredConversation(Conversation):
class Meta:
proxy = True
# this will be the 'default manager' used in the Admin, and elsewhere
objects = models.Manager()
# admin.py
#admin.register(UnfilteredConversation)
class UnfilteredConversationAdmin(Conversation):
# regular ModelAdmin stuff here
...
Or, if you have an existing ModelAdmin class you want to re-use:
admin.site.register(UnfilteredConversation, ConversationAdmin)
This approach avoids issues that can arise with overriding the default manager on the original Conversation model - because the default manager is also used in ManyToMany relationships and reverse ForeignKey relationships.
What would be so wrong with the following:
class Conversation(BaseModel):
...
deleted = models.BooleanField(default=False)
objects = models.Manager() # includes deleted conversations
nondeleted_conversations = NondeletedManager()
So in your own apps/projects, you use Conversation.nondeleted_conversations() and let the built-in admin app do it's thing.
Natan Yellin is correct, but you can change the managers order and the first will be the default, then it is the used by the admin:
class Conversation(BaseModel):
...
deleted = models.BooleanField(default=False)
all_conversations = models.Manager() # includes deleted conversations
objects = NondeletedManager()
The admin implementation of get_queryset() use ._default_manager instead .objects, as show next
qs = self.model._default_manager.get_queryset()
ref Django github BaseModelAdmin implementation
This only ensures that every time you use YourModel.objects, you will not include deleted objects, but the generic views and others uses ._default_manager too. Then if you don't override get_queryset is not a solution. I've just check on a ListView and admin.
The accepted solution works great for me but I needed a little bit more flexibility, so I ended up extending the changelist view to add in a custom queryset parameter. I can now configure my default queryset/filter as such and it can still be modified by using a different filter (get parameters):
def changelist_view(self, request, extra_context=None):
if len(request.GET) == 0 :
q = request.GET.copy()
q['status__gt'] = 4
request.GET = q
request.META['QUERY_STRING'] = request.GET.urlencode()
return super(WorksheetAdmin,self).changelist_view(request, extra_context=extra_context)
To extend on some of these answers with what I found most concise and useful.
I've made the assumption you have a field like "name" to show the entries.
# admin.py
from django.contrib import admin
#admin.register(Conversation)
class ConversationAdmin(admin.ModelAdmin):
list_display = ('name', '_is_deleted')
# Nice to have but indicates that an object is deleted
#admin.display(
boolean=True,
ordering='deleted'
)
def _is_deleted(self, obj):
return obj.deleted
def get_queryset(self, request):
return Conversation.all_conversations
Which will give you an interface like:
The problem I found with subclassing a model was that it caused issues with meta inheritance and reverse-path lookups.