Serializing a Django Queryset returns wrong data - python

I'm writing a bit of Javascript for my app and I need to get a queryset to my .js file by serializing to JSON data first and then returning it to the HTML template. I followed the documentation and I thought I got it working the first time but I now realized that the data it returns aren't the same with what there is in the database.
Here's my views.py:
class DetailView(generic.DetailView):
model = Poll
template_name = 'voting/detail.html'
context_object_name = 'question'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
#note this line below
context['data'] = serializers.serialize("json", Profile.objects.filter(user_id=self.request.user))
return context
def get_queryset(self):
"""
Excludes any questions that aren't published yet.
"""
return super(DetailView, self).get_queryset().filter(
eligiblevoters__user=self.request.user,
pub_date__lte=timezone.now()
)
def get(self, request, *args, **kwargs):
try:
return super(DetailView, self).get(request, *args, **kwargs)
except Http404:
return render(request, 'voting/not_invited_to_poll.html', {})
Basically I'm putting into context a JSON serialized Queryset that contains data about the logged in user from the Profile model.
The profile model in models.py:
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
email_confirmed = models.BooleanField(default=False)
encrypted_private_key = models.BinaryField(max_length=500, blank=True)
public_key = models.BinaryField(max_length=30, blank=True)
salt = models.BinaryField(max_length=16, blank=True)
The output serialized data that is accessible in my HTML file is this
which looks correct at first but I noticed that even though the user id is correct the fields encrypted_private_key, public_key and salt are nothing like in the database.
The same queryset exported from my database for user id 14 is this:
I now it looks like a lot of data but just notice that the values for the fields encrypted_private_key, public_key and salt are completely different with what I get in JSON.
For a matter of fact if I remove the filtering of my table in my view like so:
class DetailView(generic.DetailView):
model = Poll
template_name = 'voting/detail.html'
context_object_name = 'question'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
#I change the line below to return the whole table
context['data'] = serializers.serialize("json", Profile.objects.all())
return context
def get_queryset(self):
"""
Excludes any questions that aren't published yet.
"""
return super(DetailView, self).get_queryset().filter(
eligiblevoters__user=self.request.user,
pub_date__lte=timezone.now()
)
def get(self, request, *args, **kwargs):
try:
return super(DetailView, self).get(request, *args, **kwargs)
except Http404:
return render(request, 'voting/not_invited_to_poll.html', {})
Now I get the whole table back serialized and what I notice is that those three fields have the same values for all users (in all the rows) even though that's not the case at all.
In the serialized data the encrypted_private_key, public_key and salt has the same value for all users, while in the database each user has a different value.
What am I doing wrong?

Ok, so it looks like Django's JSON serializer encodes BinaryField to base64 format by default. That's why the serialized data and the values from my database are completely different. I expected to see the same values but I guess that not the case which makes sense, I just didn't realize it soon enough...

Related

How to Change Values of Serialized Data in RetrieveUpdateDestroyAPIView

First, I would like to present how my managers, models, serializers and views look like upfront.
class PublishedManager(models.Manager):
"""
Only published articles. `due_date` is past.
"""
def get_queryset(self):
now = timezone.now()
return super().get_queryset().filter(due_date__lt=now)
class UnpublishedManager(models.Manager):
"""
Only unpublished articles. `due_date` is future.
"""
def announced(self):
return self.get_queryset().filter(announced=True)
def get_queryset(self):
now = timezone.now()
return super().get_queryset().filter(due_date__gt=now)
class Article(models.Model):
content = models.TextField()
due_date = models.DateTimeField()
announced = models.BooleanField()
# managers
objects = models.Manager() # standard manager
published = PublishedManager()
unpublished = UnpublishedManager()
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ("content", "due_date")
class ArticleRUDView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
queryset = Article.objects.all()
In this code, ArticleRUDView naturally responds with all Article because of Article.objects.all(), yet this is not what I want to do. What I want to do is:
If the user is authenticated, then Article.objects.all().
If the user is anonymous,
If the entry is published (which means its due_date is less than now), then serialize all fields.
If the entry is not published (which means its due_date is greater than now), then still serialize, but content should be null in JSON.
Or, in short, how do I alter the serializer's data in a view?
Troubleshooting
This section might get updated in time. I will elaborate on what I find.
Overriding get_serializer Method from GenericAPIView
So I've found out I can get an instance of ArticleSerializer. So I did below:
def get_serializer(self, *args, **kwargs):
serializer = super().get_serializer()
if self.request.user.is_authenticated:
return serializer
obj = self.get_object() # get_object, hence the name, gets the object
due_date = obj.due_date
now = timezone.now()
if due_date > now:
serializer.data["content"] = None
return serializer
However, my tests didn't go well at all. This, oddly, returns an empty string on content field in JSON. I've tried different things but got that empty string. I do not have any single clue about what to do from here.
Environment
Python 3.7.4
Django 2.2.7
Django Rest Framework 3.10.3
I think you want to use get_serializer_class as opposed to get_serializer. You can allow the serializer class to choose what to stick in content instead of all the mucking around with managers, since you want to serialize all objects anyway. Something like this should work:
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ("content", "due_date")
class AnonymousArticleSerializer(ArticleSerializer):
content = serializers.SerializerMethodField()
#staticmethod
def get_content(obj):
if obj.due_date > timezone.now():
return None
return obj.content
class ArticleRUDView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
queryset = Article.objects.all()
def get_serializer_class(self):
if self.serializer_class:
return self.serializer_class
if self.request.user.is_authenticated:
self.serializer_class = ArticleSerializer
else:
self.serializer_class = AnonymousArticleSerializer
return self.serializer_class
One thing I don't like about this solution is that if you have a more complicated serializer field you're overwriting, you'd have to put the logic somewhere, but in this case (context being a text field) it's pretty simple.

How to retrieve current logged in user for dynamic file name use in model

I'm trying to set the current 'upload_to=' directory equal to the current logged-in user's username so that each file uploaded is saved into the user's own directory.
I have tried to follow the django documentation which looks similar to this...
from django.db import models
def user_directory_path(instance, filename):
# file will be uploaded to MEDIA_ROOT/user_<id>/<filename>
return 'user_{0}/{1}'.format(instance.user.id, filename)
class UploadReports(models.Model):
upload = models.FileField(upload_to=user_directory_path, null=True)
I have also tried to add RequestMiddleware to achieve this but it felt wrong as I was implementing it.
I want it to grab the current logged in user and use it in the directory path. The error that comes up is: AttributeError at /stylist/
'UploadReports' object has no attribute 'user'
Solution: The Django documentation does not specify a user needing to be added to the model - though it does expect one.
When it was done the model looked like this:
def user_directory_path(instance, filename):
# file will be uploaded to MEDIA_ROOT/user_<id>/<filename>
return 'uploads/{0}/{1}'.format(instance.user.username, filename)
class UploadReports(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
xls = models.FileField(upload_to=user_directory_path)
If you add the user here then DO NOT FORGET to add the user to the field of the form as such:
class DocumentForm(forms.ModelForm):
class Meta:
model = UploadReports
fields = ('xls', 'user')
Once you add the field to the form there becomes a new field in the template form with the list of possible users. As most people probably don't, I didn't want the form to include the user. Therefore, as ilja stated, you must exclude the form as such:
class DocumentForm(forms.ModelForm):
class Meta:
model = UploadReports
fields = ('xls', 'user')
exclude = ('user', ) # make sure this is a tuple
Once the form is excluded it will go back to throwing the error that the user does not exist. So you need to add the user in the post method of theviews.py as such:
class FileUploadView(View):
form_class = DocumentForm
success_url = reverse_lazy('home')
template_name = 'file_upload.html'
def get(self, request, *args, **kwargs):
upload_form = self.form_class()
return render(request, self.template_name, {'upload_form': upload_form})
def post(self, request, *args, **kwargs):
upload_form = self.form_class(request.POST, request.FILES)
if upload_form.is_valid():
form_done = upload_form.save(commit=False) # save the form but don't commit
form_done.user = self.request.user # request the user
form_done.save() # finish saving the form
return redirect(self.success_url)
else:
return render(request, self.template_name, {'upload_form': upload_form})
It is not an easy task but it is rewarding when it is done!

DRF using serializers with a dynamic primary key

Our API has a model defined:
class Job(models.Model):
file = models.FileField('File')
xml = models.FileField('XML')
There is a basic serializer:
class XmlSerializer(serializers.ModelSerializer)
file = serializers.FileField(read_only=True)
xml = serializers.FileField(required=True)
class Meta:
model = Job
fields = '__all__'
We don't want to change the file but we do want to change the xml field. The xml is uploaded by a system that doesn't know the primary key. Of course we need this to update the model.
I have the following code at the moment:
class ProcessXml(mixins.CreateModelMixin, generics.GenericAPIView):
serializer_class = XmlSerializer
def post(self, request, format=None):
pk = 200
serializer = XmlSerializer(request.data)
return Response({})
pk = 200 serves as an example instead of the code we use to parse the xml. I know this doesn't work but it's to show my intention (more or less).
I have tried to use
id = serializers.SerializerMethodField()
def get_id(self, obj):
return 200
without success.
How do I get this primary key into the the serializer?
I was making it much too difficult. The solution was pretty easy.
class ProcessXml(mixins.CreateModelMixin, generics.GenericAPIView):
serializer_class = XmlSerializer
def post(self, request, format=None):
id = magic_xml_parser_function()
job = get_object_or_404(Job, pk=id)
serializer = XmlSerializer(job, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
magic_xml_parser_function() contains the id we found in the xml. This solved it for us.

Get the django user in save while using django.form

I have a problem getting the user in django when I use django forms. My code looks something like this.
The view:
#login_required
def something(request):
item = ItemForm(request.POST)
item.save(user=request.user)
The form:
class ItemForm(ModelForm):
class Meta:
model = Item
fields = '__all__'
def save(self, *args, **kwargs):
user = kwargs['user']
super(ItemForm, self).save(user=user)
The model
class Item(models.Model):
field = models.CharField(max_length=100,)
field2 = models.CharField(max_length=100,)
def check_permissions(self, user):
return user.groups.filter(name='group').exists()
def save(self, *args, **kwargs):
if self.check_permissions(kwargs['user']):
super(Item, self).save()
My problem is that when I call the default save in ItemForm I get an error because the user param is unexpected. I need the user in the model to make the permission check but I dont know how to get it.
I finally solved the problem. The way I found was to save the form without the user but with the commit flag set to False and then calling the function save from the model with the user param.
The form save method now looks like this
def save(self, *args, **kwargs):
item = super(ItemForm, self).save(commit=False)
item.save(user=kwargs['user'])

django form exclude a user instance from a queryset

I have the following model:
class UserProfile(models.Model):
user = models.OneToOneField(User, related_name="user")
people_interested = models.ManyToManyField(User, related_name="interested")
Now I want a form where I want to offer users a form where they can choose people_interested, so I add the following forms.py
class ChooseForm(forms.Form):
q_set = User.objects.all()
peers = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset = q_set)
and then in views:
form = ChooseForm(data = request.POST or None)
if request.POST and form.is_valid():
uprofile, created = UserProfile.objects.get_or_create(user=request.user)
uprofile.people_interested = form.cleaned_data['peers']
uprofile.save()
return HttpResponseRedirect("/")
else:
return render(request, "form_polls.html", {'form':form})
But the trouble with this is, the current user instance also gets displayed. So I tried the following in views.py:
form = ChooseForm(request.user.id, data = request.POST or None)
and then in forms.py
class ChooseForm(forms.Form):
def __init__(self, uid, *args, **kwargs):
super(ChooseForm, self).__init__(*args, **kwargs)
self.fields['peers'].queryset = User.objects.exclude(id=uid)
q_set = User.objects.all()
peers = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset = q_set)
But the above is not a clean implementation, is there a better method of doing it??
What makes you say this is not a clean implementation? Overwriting queryset on __init__ is perfectly acceptable.
The only things I'd do to improve your code is using a post_save signal on User to create it's UserProfile, then just do user.get_profile() on your view. See this question
You could also use a ModelForm for UserProfile instead of a regular form, and limit the fields to people_interested.

Categories