Edited Title : Limit multiple "Many to One" fields into "One to One" association : Django
We've Book, User and Rating as Django model.
Each Book has many Ratings
Each Rating has one Book
Each User has many Ratings
Each Rating has one User
For Book
class Book(models.Model):
isbn = models.CharField(max_length=32)
title = models.CharField(max_length=256)
def __str__(self):
return self.title
For Book Rating
class BookRating(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
rating = models.SmallIntegerField(choices=[(i, i) for i in range(1, 6)])
def __str__(self):
return self.rating
Problem Statement
How can I ensure that each User has atmost one rating on each book?
Just do
class BookRating(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
rating = models.SmallIntegerField(choices=[(i, i) for i in range(1, 6)], default=1)
class meta:
unique_together = ('book','user')
Your models do implement a many to many relationship, however you are not getting full access to the django ManyToMany functionality. I recommend you do something like this:
class Book(models.Model):
isbn = models.CharField(max_length=32)
title = models.CharField(max_length=256)
ratings = models.ManyToManyField(User,through='BookRating')
When you do this your BookRating can remain unchanged, but the small change to the Book model gives you full access to the api described here: https://docs.djangoproject.com/en/1.10/topics/db/examples/many_to_many/
What's interesting is that modifying the Book model as described above does not make any changes to your table structures in your database. They remain unchanged. It's simply a matter of unlocking the api.
Related
i have a model course and i also have a model review and rating that allows users rate a particular course. now i want to count all the ratings on each course when i filter. NOTE: in the courses detail view i figured out a way to count the ratings like this rating_count = CourseRating.objects.filter(course=course, rating__in=["3.0", "4.0", "5.0"]).count(). This only worked in the detail view because i get the course first using course = Course.objects.get(slug=course_slug).
Now i want to count the rating in course lists view, how can i do this while using filter?
this is how the detail view looks like
#login_required
def course_details(request, course_slug):
user = request.user
course = Course.objects.get(slug=course_slug)
reviews_count = CourseRating.objects.filter(active=True, course=course).count()
rating_count = CourseRating.objects.filter(course=course, rating__in=["3.0", "4.0", "5.0"]).count()
this is how the list view look like NOTE: this is where i want to count all the rating of each course
def index(request):
courses = Course.objects.filter(course_publish_status="published").order_by('?')
models.py
class Course(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
course_title = models.CharField(max_length=100, null=True, blank=True)
slug = models.SlugField(unique=True)
course_category = models.ForeignKey(Category, on_delete=models.DO_NOTHING, null=True, blank=True, related_name="courses")
course_publish_status = models.CharField(max_length=10000, choices=COURSE_PUBLISH_STATUS, default="in_review")
class CourseRating(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
course = models.ForeignKey(Course, on_delete=models.CASCADE, null=True, related_name="ratings")
rating = models.CharField(max_length=1000, choices=USER_COURSE_RATING)
review = models.TextField()
date = models.DateTimeField(auto_now_add=True)
active = models.BooleanField(default=False)
def __str__(self):
return f"{self.course.course_title} - {self.rating}"
class Meta:
verbose_name = "Course Reviews and Ratings"
What you're looking for is Aggregation the docs have some pretty good (and quick) examples of how to achieve it.
You're looking to use an .annotate with the Count object. Judging by your filtering for ratings, you'll also want to use the filter= parameter of the Count object.
I also highly suggest you look into how to properly use the Q() object.
With all that being said, an example to achieve what you're looking for:
courses = Course.objects.filter(
course_publish_status="published",
).annotate(
rating_count=Count(
'ratings',
filter=Q(ratings__rating__in=["3.0", "4.0", "5.0"])
)
).order_by("?")
Keep in mind that I considered you have a related_name="ratings" on your ForeignKey. For your next questions I strongly suggest sharing the models you're working with as well (or at least the relevant portions of them).
I working on an app using django and python at school. It an app for writing reviews and rating movies. I would like to implement a rating system, so I can display the average rating for a film based on the rating given in the reviews. At the moment this code gives the rating in descending order for all reviews, and not the average. Like if I got two reviews for a movie, with score 1 and 5, I get both, but not one that says 3. Please help!
In models.py:
class Film(models.Model):
title = models.CharField(max_length=100)
title_short = models.CharField(max_length=17, default=None, null=True)
plot = models.TextField()
poster = models.ImageField(default="default.png", upload_to="posters")
release_date = models.DateField(blank=True, null=True)
date_posted = models.DateTimeField(default=timezone.now)
class Review(models.Model):
writer = models.ForeignKey(User, on_delete=models.CASCADE)
reviewed_film = models.ForeignKey(Film, on_delete=models.CASCADE)
title = models.CharField(max_length=100)
content = models.TextField()
rating = models.IntegerField(
default=1, validators=[MinValueValidator(1), MaxValueValidator(5)]
)
In views.py:
class ToplistListView(LoginRequiredMixin, ListView):
model = Review
template_name = "board/toplist.html"
context_object_name = "films"
def get_queryset(self):
return Review.objects.annotate(avg_rating=Avg('rating')).order_by('-avg_rating')
You're getting the average rating on each review, which will of course just give you the rating for that review as each review only has a single rating. The average of a single number is just that number.
I think what you want is the average for all the ratings for a film. So you'd want a query that looks something like:
Flim.objects.annotate(avg_rating=Avg('review_set__rating')).order_by('-avg_rating')
Also, you can change the name of review_set (which is default for a Foreign Key lookup if no related name is set) to whatever you want by setting related_name='reviews' or whatever you want it to be called on the FK definition.
So:
reviewed_film = models.ForeignKey(Film, related_name='reviews', on_delete=models.CASCADE)
ETA: if you make that change, your query would become:
Flim.objects.annotate(avg_rating=Avg('reviews__rating')).order_by('-avg_rating')
I have models of Exercise, Training and Workout.
Training contains some exercises (Exercise)
Workout contains trainings (Training).
Snippet of my models.py:
class Exercise(models.Model):
user = models.ForeignKey(User, related_name='exercises',
on_delete=models.CASCADE)
name = models.CharField(max_length=80)
description = models.TextField(max_length=300)
details = models.ManyToManyField(ExerciseDetail, blank=True)
...
class Training(models.Model):
user = models.ForeignKey(User, related_name='trainings',
on_delete=models.CASCADE)
name = models.CharField(max_length=80)
description = models.CharField(max_length=250)
exercises = models.ManyToManyField(Exercise, related_name='trainings',
blank=True)
...
class Workout(models.Model):
user = models.ForeignKey(User, related_name='workouts',
on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now=True)
name = models.CharField(max_length=200)
description = models.TextField(max_length=400, blank=True)
trainings = models.ManyToManyField(Training, related_name='workouts',
blank=True)
...
I would like to have possibility to use something like Workout.objects.get(name='workout').exercises.objects.all() to get a list/set of all exercises included in trainings of chosen Workout.
I would also like to have possibility to use exercises`` field with Django Rest Framework to list all exercises, possibly with link to particularExercise``` model serializer.
Can someone give a hint how can I do that?
You can query this with:
Exercise.objects.filter(
trainings__workouts__name='workout'
)
With the consecutive underscores (__), you thus can look "through" relations.
This will thus return the Exercises that belong to Trainings that belong to Workouts with as name 'Workout'.
I am trying to create the proper Django model that could fit the following reqs:
Person Class has 1 to many relations with the Address Class
Person Class has many to many relations with the Group Class
Book Class contains the collections of the Persons and the Groups
This is my code:
class Person(models.Model):
first_name = models.CharField(max_length=15)
last_name = models.CharField(max_length=20)
def __str__(self):
return self.first_name+ ' - ' + self.last_name
class Address(models.Model):
person = models.ForeignKey(Person)
address_line = models.CharField(max_length=200)
def __str__(self):
return self.address_line
class Group(models.Model):
group_name = models.CharField(max_length=12)
persons = models.ManyToManyField(Person)
def __str__(self):
return self.group_name
class Book(models.Model):
record_name = models.CharField(max_length=12)
person = models.ForeignKey(Person )
group = models.ForeignKey(Group )
def __str__(self):
return self.record_name
However it's not correct:
1) A Group can now contain multiple Persons but the Persons do not contain any Group.
I am not sure if I should add to the Person class the following code:
groups = models.ManyToManyField(Group)
2) The Book class now contains only 1 record of Person & Group per Book record.
3) When I added the Foreign Keys to the models, I removed
on_delete tag:
person = models.ForeignKey(Person, on_delete=models.CASCADE())
because it does not compile it, asking for some params.
I know how to make all this for C#, but I am a kinda stucked with this simple task in Python/Django.
1) The ManyToMany field should appear only in one of the models, and by looks of things you probably want it in the Person model.
Its important to understand that the data about the ManyToMany field is saved in a differant table. Django only allows this field to be visable through buth models (so basiclly, choose where it is move convinient).
2)By the look of your structure I will suggest you use a ManyToMany field through a different table. here is an example:
class Activity(models.Model):
name = models.CharField(max_length=140)
description = models.TextField(blank=True, null=True)
class Route(models.Model):
title = models.CharField(max_length=140)
description = models.TextField()
activities_meta = models.ManyToManyField(Activity, through = 'RouteOrdering')
class RouteOrdering(models.Model):
route = models.ForeignKey(Route, on_delete=models.CASCADE)
activity = models.ForeignKey(Activity, on_delete=models.CASCADE, related_name='activita')
day = models.IntegerField()
order = models.IntegerField(default=0)
that way the data is binded to the ManyToMany field
Below is my Django models code
from django.db import models
class BookUser(models.Model):
email= models.CharField(max_length=254,primary_key=True) #mail address key
name = models.CharField(max_length=254) #max 64 char (lower case?)
contact= models.CharField(max_length=12)
imei = models.CharField(max_length=16) #imei number
address= models.TextField() #list of address ids
booksInShelf:[] #list of user book's unique ids
booksUnderCirculation:[] #list of user book's unique ids
class Meta:
ordering = ('email',)
class Book(models.Model):
isbn = models.CharField(max_length=13)
title=models.CharField(max_length=500)
description =models.TextField()
author = models.CharField(max_length=200)
userRating = models.CharField(max_length=1)
users = #list of user ids hold this book in shelf
class UserBook(models.Model):
#id: generated by django
bookId: #id of parent book
rent= models.BooleanField(default=False) #boolean is ready to rent
sell= models.BooleanField(default=False) #boolean is ready to sell
price =models.FloatField() #selling price
rentBase=models.FloatField() #base price of rent
rentPeriod=models.IntegerField() #days after which extra rent would apply
dateModified =models.DateTimeField(auto_now=True) #track date it came into shelf
dateAdded = models.DateTimeField(auto_now_add=True)
Here BookUser is the actual user who has some books in two categories i.e booksinShelf and bookUnderCirculation
class Book is central repository of all books, I need to define a one to many relation to BookUser.What is the easy way to do this?
User Book is specific to BookUser and it should be uniquely pointing to Class Book , So its many to one relation to Book Class.
I am confused on how to handle ids of UserBook and Book?
Also how to store the list of ids of UserBooks in class BookUser??
After looking at the Models and explanation provided below the Book model the users field should have ForeignKey relationship with the BookUser model.
so Book model should look like
class Book(models.Model):
isbn = models.CharField(max_length=13)
title=models.CharField(max_length=500)
description =models.TextField()
author = models.CharField(max_length=200)
userRating = models.CharField(max_length=1)
users = models.ForeignKey(BookUser, null=True, blank=True)
if you are using Postgresql and if you just need the pk list of booksInShelf and booksUnderCirculation then your BookUser model should look like
class BookUser(models.Model):
email= models.CharField(max_length=254,primary_key=True)
name = models.CharField(max_length=254)
contact= models.CharField(max_length=12)
imei = models.CharField(max_length=16)
address= models.TextField()
booksInShelf = models.ArrayField(models.IntegerField())
booksUnderCirculation = models.ArrayField(models.IntegerField())
and if you wish to have the full information of booksInShelf and booksUnderCirculation (not just the pk but other information related to the book as well), then you need to define it as ManyToMany relation.
class BookUser(models.Model):
email= models.CharField(max_length=254,primary_key=True)
name = models.CharField(max_length=254)
contact= models.CharField(max_length=12)
imei = models.CharField(max_length=16)
address= models.TextField()
booksInShelf = models.ManyToMany(UserBook)
booksUnderCirculation = models.ManyToMany(UserBook)
also rather than creating two ManyToMany fields in the BookUser model you can have two flags in your UserBook model called is_in_shelf and is_under_circulation. These fields would be BooleanField, you can check more about the model fields in Django Documentation here: https://docs.djangoproject.com/en/1.10/topics/db/models/#fields
This should do what you want :
class UserBook(models.Model):
bookId = models.ForeignKey('Book')
Here a UserBook has a reference to a Book item, and severals users can have the same book, but it's still a unique reference in you table Book.
Hope it helps