how to pivot with django pandas using foreign keys - python

I am using django-panda to make a dynamic table and I have achieved it, the problem I have is that I would like to add more rows to the table using foreign key relationships, in the documentation it says: "to span a relationship, just use the field name of related fields in models, separated by double underscores, "however this does not work with to_pivot_table. I did a test with to_dataframe and here the logic if it gives results, but what I need is to implement it in the pivot
these are my models:
class Student(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True,related_name='student')
quizzes = models.ManyToManyField(Quiz, through='TakenQuiz')
interests = models.ManyToManyField(Subject, related_name='interested_students')
def get_unanswered_questions(self, quiz):
answered_questions = self.quiz_answers \
.filter(answer__question__quiz=quiz) \
.values_list('answer__question__pk', flat=True)
questions = quiz.questions.exclude(pk__in=answered_questions).order_by('text')
return questions
def __str__(self):
return self.user.username
class TakenQuiz(models.Model):
student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='taken_quizzes')
quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE, related_name='taken_quizzes',verbose_name='Examen')
score = models.FloatField()
date = models.DateTimeField(auto_now_add=True)
objects = DataFrameManager()
and my views
class ResultadosTotales(ListView):
def get(self,request,*args,**kwargs):
examen=TakenQuiz.objects.all()
qs=TakenQuiz.objects.all()
df2=taken.to_dataframe(['student__user__first_name' ,'quiz', 'score'], index='student')
print(df) #here I did the test with the related attribute and if it works
rows = ['student','student__user__first_name'] #Here it does not work even though the documentation uses the same logic
rows = ['student']
cols = ['quiz']
print(df2)
pt = qs.to_pivot_table(values='score', rows=rows, cols=cols,fill_value="falta")
pt=pt.to_html(classes="table table-striped border")
context={ 'pivot': pt
}
return render(request, 'classroom/teachers/resultados_totales.html',context )
espero puedan ayudarme

Related

How to aggregate annotate fields of related models in django

My problem is a little more complicated, but I'm posting the question in the simplest possible way.
Annotated total_score in Photo.
I would like to annotate max_total_score in Person.
I wrote get_queryset of PersonManager, but the following error occurred.
Is there a new way?
models.py
from django.db import models
class PersonManager(models.Manager):
def get_queryset(self):
photos_prefetch = models.Prefetch(
'photos',
Photo.objects.annotate(total_score=models.Sum('features__score'))
)
return super().get_queryset() \
.prefetch_related(photos_prefetch) \
.annotate(max_total_score=models.Max('photos__total_score'))
class Person(models.Model):
objects = PersonManager()
class Photo(models.Model):
person = models.ForeignKey(Person, models.CASCADE, related_name='photos')
class Feature(models.Model):
photo = models.ForeignKey(Photo, models.CASCADE, related_name='features')
name = models.CharField(max_length=100)
score = models.IntegerField()
shell
>> Person.objects.all()
# Unsupported lookup 'total_score' for BigAutoField or join on the field not permitted
I solved it by using the values ​​method of the queryset.
class PersonManager(models.Manager):
def get_queryset(self):
photos = Photo.objects.annotate(total_score=models.Sum('features__score'))
photos_prefetch = models.Prefetch(
'photos',
photos
)
return super().get_queryset() \
.prefetch_related(photos_prefetch) \
.annotate(max_total_score=models.Max(photos.values('total_score')))

How to create a model field that is an average of all of another foreign key model' field. Ex: avg rating field

I have two models. Fiction model that can be any movie, book, tv series or something similar. I have another model that is a review that contain review like fields for example: reviewer, rating, description.
What I want to do is the following:
Have Two extra fields in the fiction model that are:
number of reviews
average of review ratings
I was able to add them as integer and float fields and then changed them whenever a new review was added, edited, or deleted but there are two issues.
Adding reviews from the admin won't be accounted for
I just feel like that is not the best approach and I feel like there is a more sensible approach for the fields to be automatically filled from the review model.
Here are the two models as well as how I implemented the api views.
fiction models.py
from django.db import models
from stream.models import StreamPlatform
class Fiction(models.Model):
"""
Model that encopasses a Movie, TV Series, book or similar
"""
MOVIE = 1
TV_SERIES = 2
BOOK = 3
PODCAST = 4
TYPE = (
(MOVIE, 'Movie'),
(TV_SERIES, 'TV-Series'),
(BOOK, 'Book'),
(PODCAST, 'Podcast')
)
title = models.CharField(max_length=50)
description = models.CharField(max_length=200)
active = models.BooleanField(default=True)
created = models.DateTimeField(auto_now_add=True)
avg_rating = models.FloatField(default=0)
number_rating = models.IntegerField(default=0)
platform = models.ForeignKey(
StreamPlatform,
on_delete=models.SET_NULL,
related_name='fictions',
null = True
)
type = models.PositiveSmallIntegerField(
choices = TYPE,
default = MOVIE
)
def __str__(self):
return self.title
Review models.py
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
from django.contrib.auth.models import User
from fiction.models import Fiction
class Review(models.Model):
"""
model for fiction reviews from users
"""
reviewer = models.ForeignKey(User, on_delete=models.CASCADE)
rating = models.PositiveSmallIntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
fiction = models.ForeignKey(Fiction, on_delete=models.CASCADE, related_name="reviews")
description = models.CharField(max_length=200, null = True, blank =True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return str(self.rating) + " | " + str(self.fiction)
class Meta:
unique_together = ['reviewer', 'fiction']
ordering = ['-created']
review views.py
from watchmate.permissions import IsAdminOrReadOnly, IsOwnerOrReadOnly
from rest_framework import generics, mixins
from rest_framework.exceptions import ValidationError
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django.db.utils import IntegrityError
from fiction.models import Fiction
from .models import Review
from .serializers import ReviewSerializer
class ReviewList(generics.ListCreateAPIView):
permission_classes = [IsAdminOrReadOnly]
# queryset = Review.objects.all()
serializer_class = ReviewSerializer
def get_queryset(self):
pk = self.kwargs['pk']
return Review.objects.filter(fiction=pk)
def perform_create(self, serializer):
pk = self.kwargs.get('pk')
fiction = Fiction.objects.get(pk=pk)
reviewer = self.request.user
# check if user has already reviewd this fiction
# review = Review.objects.filter(fiction=fiction, reviewer=reviewer)
# if review.exists():
# raise ValidationError("You have already reviewed this fiction")
try:
serializer.save(fiction=fiction, reviewer=reviewer)
if fiction.number_rating == 0:
fiction.avg_rating = serializer.validated_data['rating']
else:
fiction.avg_rating = (serializer.validated_data['rating']+fiction.avg_rating)/2
fiction.number_rating += 1
fiction.save()
except IntegrityError:
raise ValidationError("You have already reviewed this fiction")
class ReviewDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Review.objects.all()
serializer_class = ReviewSerializer
permission_classes = [IsOwnerOrReadOnly]
def perform_update(self, serializer):
instance = self.get_object()
pk = self.kwargs.get('fiction_pk')
fiction = Fiction.objects.get(pk=pk)
# calculate overall rating sums
total_rating = fiction.avg_rating*fiction.number_rating
# subtract old rating
total_rating -= instance.rating
# add new rating
total_rating += serializer.validated_data['rating']
# calculate new fiction avg rating and save it
fiction.avg_rating = total_rating/fiction.number_rating
fiction.save()
super().perform_update(serializer)
def perform_destroy(self, instance):
instance = self.get_object()
pk = self.kwargs.get('fiction_pk')
fiction = Fiction.objects.get(pk=pk)
# calculate overall rating sums
total_rating = fiction.avg_rating*fiction.number_rating
# subtract old rating
total_rating -= instance.rating
# decrease fiction reviews by one
fiction.number_rating -= 1
# calculate new fiction avg rating and save it
fiction.avg_rating = total_rating/fiction.number_rating
fiction.save()
super().perform_destroy(instance)
The logic for adding a new reviews is not 100% correct but never mind it. I just want to know how to be able to implement this logic at models and field level so it can also be possible to add reviews using admin and still be able to see changes
As said in Willem's comment, you can use #property. Here is an example from the official documentation:
class Person(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
birth_date = models.DateField()
def baby_boomer_status(self):
"Returns the person's baby-boomer status."
import datetime
if self.birth_date < datetime.date(1945, 8, 1):
return "Pre-boomer"
elif self.birth_date < datetime.date(1965, 1, 1):
return "Baby boomer"
else:
return "Post-boomer"
#property
def full_name(self):
"Returns the person's full name."
return '%s %s' % (self.first_name, self.last_name)
In case anyone wanted a solution to such a problem that is how I solved mine by adding a property to method just like #Willem Van Onsem and #Mhamed Bendenia said.
#property
def number_of_ratings(self):
return self.reviews.count()
#property
def average_rating(self):
return self.reviews.aggregate(Avg('rating'))['rating__avg']
This way I dont have to write the logic myself in every view

How can I make DRF Serializer create() function only create an entry that does not exist yet?

I have two tables, which are connected with each other through a cross table. (Recipes <--> Ingredients)
My Serializer works ok, I can send POST-Requests and it saves everything. The problem ist, that every time a new Recipe comes in with let just say the Ingredient "Milk" then my Serializer creates a new entry in my database named Milk, although I have an already existing entry "Milk" in my database.
How do I tell my Serializer to use the Id of an already existing entry instead of creating a new one every time for the cross table.
Here is how I thought I could fix it, but it clearly doesn't:
class RecipeIngredientSerializer(serializers.ModelSerializer):
ingredient = IngerdientSerializer()
class Meta:
model = recipe_ingredients
fields = ['amount', 'unit', 'ingredient']
def create(self, validated_data):
ingredient_validated_data = validated_data.pop('ingredient')
ingredient_serializer = self.fields['ingredient']
ingredientDict = dict(ingredient_validated_data)
// This is where I try to check if there is already an ingredient with the name from the form
ingredientObj = ingredient.objects.all().filter(ingredient_name=ingredientDict['ingredient_name']).
if not ingredientObj:
ingredient_instance = ingredient.objects.create(**ingredientDict)
validated_data['ingredient'] = ingredient_instance
else:
ingredient_instance = ingredient_serializer.create(ingredientDict)
validated_data['ingredient'] = ingredient_instance
recipe_ingredients_instance = recipe_ingredients.objects.create(**validated_data)
return recipe_ingredients_instance
This code also seems to work, at least I find an existing ingredient, but after the last create() it seems to ignore what ever I push into the validated_data['ingredient'] object.
EDIT
my models are:
class recipe_ingredients(models.Model):
recipe = models.ForeignKey(recipe, models.CASCADE)
ingredient = models.ForeignKey(ingredient, models.CASCADE)
amount = models.IntegerField(default=0)
unit = models.CharField(max_length=50)
def __str__(self):
return self.ingredient.ingredient_name + ' of Recipe: ' + self.recipe.recipe_name
class recipe(models.Model):
recipe_name = models.CharField(max_length=200)
assembly_time = models.IntegerField(default=0)
number_of_servings = models.IntegerField(default=0)
tags = models.ManyToManyField(tag, blank=True)
def __str__(self):
return self.recipe_name
class ingredient(models.Model):
ingredient_name = models.CharField(max_length=200)
ingredient_calories = models.IntegerField('Calories per 100 Units', default=-1)
default_unit = models.CharField(max_length=50)
def __str__(self):
return self.ingredient_name
I got the answer, finally. My mistake is this line in my Serializer:
ingredientObj = ingredient.objects.all().filter(ingredient_name=ingredientDict['ingredient_name']).
if not ingredientObj:
ingredient_instance = ingredient.objects.create(**ingredientDict)
validated_data['ingredient'] = ingredient_instance
I changed it now so that it looks something like this:
ingredientObj = ingredient.objects.all().filter(ingredient_name=ingredientDict['ingredient_name']).
if len(ingredientObj):
ingredient_instance = ingredientObj.first()
validated_data['ingredient'] = ingredient_instance
The ingredient.object.create(**ingredientDict) does actually create a new object (who would have known ;) ). This is probably still an ugly solution and I am open to more criticism but this does work for now.

Django model reference and manipulation

I have the following models in Django that have a structure as follows:
class Office_Accounts(models.Model):
accountid = models.EmailField(max_length=200, unique=True)
validtill = models.DateField(default=datetime.now)
limit = models.CharField(max_length=2)
class Device(models.Model):
device_type = models.ForeignKey(DeviceType,to_field='device_type')
serial_number = models.CharField(max_length=200,unique=True)
in_use_by = models.ForeignKey(User,to_field='username')
brand = models.CharField(max_length=200,default="-", null=False)
model = models.CharField(max_length=200,default="-", null=False)
type_number = models.CharField(max_length=200,blank=True,null=True, default = None)
mac_address = models.CharField(max_length=200,blank=True,null=True, default = None)
invoice = models.FileField(upload_to='Device_Invoice', null=True, blank = True)
msofficeaccount = models.ForeignKey(Office_Accounts, to_field="accountid")
class Meta:
verbose_name_plural = "Devices"
def full_name(self):
return self.device_type + self.serial_number + self.brand
I will display both of the models in admin.py.
Now, I want to display the count of each accountid present in the field "msofficeaccount" (present in Device Models) in my admin page of Office_Accounts model. For an example if xyz#abc.com appears in 10 rows of msofficeaccount field then, the count should be displayed as 10 in Office_Accounts admin page. Can anyone please guide me how should I approach this problem to solve it?
You could add a method to your admin class that returns the count of related devices for each office_account, but that would be very inefficient. Instead you can override get_queryset to annotate the count from a database aggregation function:
from django.db.models import Count
class Office_AccountsAdmin(admin.ModelAdmin):
list_display = (..., 'device_count')
...
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.annotate(device_count=Count('device'))
(On a minor note, Python style is always to use CamelCase for class names, and Django style is to use singular model names, so your model should really be called OfficeAccount.)

Django-Tables2 add extra columns from dictionary

I apologize if this question has been asked before but I couldn't find my specific use case answered.
I have a table that displays basic product information. Product details such as price, number of sales, and number of sellers are scraped periodically and stored in a separate database table. Now I want to display both the basic product information and scraped details in one table on the frontend using tables2. To do this, I wrote a function in my Product model to fetch the latest details and return them as a dictionary this way I can use a single Accessor call.
# models.py
class Product(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=256)
brand = models.ForeignKey(Brand)
category = models.CharField(max_length=128, choices=CATEGORY_CHOICES)
def __unicode__(self):
return self.name
def currentState(self):
currentDetailState = ProductDetailsState.objects.filter(
product=self
).latest('created_at')
# return current details as a dictionary
return {
price: currentDetailState.price,
num_sellers: currentDetailState.num_sellers,
num_sales: currentDetailState.num_sales
}
class ProductDetailsState(models.Model):
product = models.ForeignKey(Product)
created_at = models.DateTimeField(auto_now_add=True)
price = models.DecimalField(max_digits=6, decimal_places=2, null=True)
num_sellers = models.IntegerField(null=True)
num_sales = models.IntegerField(null=True)
def __unicode__(self):
return self.created_at
# tables.py
class ProductTable(tables.Table):
productBrand = tables.Column(
accessor=Accessor('brand.name'),
verbose_name='Brand'
)
currentRank = tables.Column(
accessor=Accessor('currentRank')
)
class Meta:
model = Product
...
How do I now use this returned dictionary and split it into columns in my Product table? Is there another way to use an Accessor than how I am doing it?
You can use the Accessor to traverse into the dict, so something like this should work:
class ProductTable(tables.Table):
# brand is the name of the model field, if you use that as the column name,
# and you have the __unicode__ you have now, the __unicode__ will get called,
# so you can get away with jus this:
brand = tables.Column(verbose_name='Brand')
currentRank = tables.Column()
# ordering on the value of a dict key is not possible, so better to disable it.
price = tables.Column(accessor=tables.A('currentState.price'), orderable=False)
num_sellers = tables.Column(accessor=tables.A('currentState.num_sellers'), orderable=False)
num_sales = tables.Column(accessor=tables.A('currentState.num_sales'), orderable=False)
class Meta:
model = Product
While this works, sorting is also nice to have. In order to do that, your 'currentState' method is a bit in the way, you should change the QuerySet you pass to the table. This view shows how that could work:
from django.db.models import F, Max
from django.shortcuts import render
from django_tables2 import RequestConfig
from .models import Product, ProductDetailsState
from .tables import ProductTable
def table(request):
# first, we make a list of the post recent ProductDetailState instances
# for each Product.
# This assumes the id's increase with the values of created_at,
# which probably is a fair assumption in most cases.
# If not, this query should be altered a bit.
current_state_ids = Product.objects.annotate(current_id=Max('productdetailsstate__id')) \
.values_list('current_id', flat=True)
data = Product.objects.filter(productdetailsstate__pk__in=current_state_ids)
# add annotations to make the table definition cleaner.
data = data.annotate(
price=F('productdetailsstate__price'),
num_sellers=F('productdetailsstate__num_sellers'),
num_sales=F('productdetailsstate__num_sales')
)
table = ProductTable(data)
RequestConfig(request).configure(table)
return render(request, 'table.html', {'table': table})
This simplifies the table definition, using the annotations created above:
class ProductTable(tables.Table):
brand = tables.Column(verbose_name='Brand')
currentRank = tables.Column()
price = tables.Column()
num_sellers = tables.Column()
num_sales = tables.Column()
class Meta:
model = Product
You can find the complete working django project at github

Categories

Resources