I'm new to Django, moved from PHP with Propel ORM engine.
So here is what I am currently doing in Django. My website has several models like
Book,
Publisher,
Comment,
Article and so on (it's not the point)
Each of them can can be
liked or disliked by a user (only once) changing the model's rating by +1 or -1.
In terms of PHP i would create a behavior, for ex. "Rateable" which would add some fields and methods to the original model and query class (like get_rating(), order_by_rating(), etc) and create a separate table for each model, for ex. book_rating_history which would hold all of the ratings for each object, to determine if the user can or can't change the rating (or show all object's ratings, if necessary). So all I would need to do is to specify the "Rateable" behavior in the model declaration, and that's all. Everything else is done automatically.
The question is - how to solve this in Django? How to model correctly? Which techniques do you use in similar cases?
You'll want to store ratings and books separately, something like this (untested).
from django.contrib.auth.models import User
from django.db import models
from django.db.models import Sum
class BookRating(models.Model):
user = models.ForeignKey(User)
book = models.ForeignKey('Book')
# you'll want to enforce that this is only ever +1 or -1
rating = models.IntegerField()
class Meta:
unique_together = (('user', 'book'),)
class Book(models.Model):
title = models.CharField(max_length = 50)
def rating(self):
return BookRating.objects.filter(book = self).aggregate(Sum('rating'))
unique_together enforces that each user can only rate a given book once.
You can then use this something like:
book = Book.objects.get(pk = 1)
rating = book.rating()
Add a comment if you have problems with this - I've not tested it, but hopefully this is enough to get you started.
You can probably avoid each object (books, publishers, comments, articles) having a separate rating object using content types, if you want.
Alternatively, you might consider looking at existing reusable apps that handle liking, like django-likes or phileo.
You can define special methods (for example vote, get_rating, etc.) only onсe in abstract model and then create your "Rateable" models using this one.
class Rateable(models.Model):
class Meta:
abstract = True
def vote(self, *args, **kwargs):
...
def rating(self, *args, **kwargs):
...
class Book(Rateable):
...
Also it is better to use single model for storing rating data witch content types as noticed Dominic Rodger
Related
I'm working on django models. At the design level I'm confused if I can implement functions inside model class. If I can implement then what kind of functions should be going inside and what kind of functions shouldn't. I couldn't find a document regarding this apart from doc
Or is there any document where I can figure out about this?
Yes, of course you can create functions inside the model class. It's highly recommended especially for things that you have to calculate specifically for objects of that model.
In example it's better to have function that calculates let's say Reservation time. You don't have to put that info inside database, just calculate only when it's needed:
class Reservation(models.Model):
valid_to = models.DateTimeField(...)
def is_valid(self):
return timezone.now() < self.valid_to
Depending on what you actually need/prefer it might be with #property decorator.
I guess you are asking about the old discussion "Where does the business logic go in a django project? To the views, or the model?"
I prefer to write the business logic inside of the views. But if it happens that I need a special "treatment" of a model several times in multiple views, I turn the treatment inside of the model.
To give you an example:
# models.py
class Customer(models.Model):
name = models.CharField(max_length=50, verbose_name='Name')
# views.py
def index(request):
customer = Customer.objects.all().first()
name = str.upper(customer.name) # if you need that logic once or twice, put it inside of the view
return HttpResponse(f"{name} is best customer.")
If you need the logic in multiple views, over and over again, put it inside of your model
# models.py
class Customer(models.Model):
name = models.CharField(max_length=50, verbose_name='Name')
#property
def shouted_name(self):
return str.upper(self.name)
# views.py
def index(request):
customer = Customer.objects.all().first() # grab a customer
return HttpResponse(f"{customer.shouted_name} is best customer.")
def some_other_view(request):
customer = Customer.objects.all().first() # grab a customer
customer_other = Customer.objects.all().last() # grab other customer
return HttpResponse(f"{customer.shouted_name} yells at {customer_other}")
# see method shouted_name executed in two views independently
I have a question whether or not it is possible to use the generic UpdateView class to edit "both sides" of a many-to-many relationship.
I have the following classes defined in models.py:
class SomeCategory(models.Model):
code = models.CharField(max_length=5)
name = models.CharField(max_length=40)
class SomeClass(models.Model):
code = models.CharField(max_length=3, unique=True)
name = models.CharField(max_length=30, unique=False)
age = models.IntegerField(null=False)
allowed_categories = models.ManyToManyField(SomeCategory)
These are both dictionary type tables that store sets of configuration data for my application. To allow editing the dictionaries I use simple UpdateViews:
class SomeClassUpdate(UpdateView):
model = SomeClass
template_name = 'admin/edit_class.html'
fields = ['code', 'name', 'age', 'allowed_categories']
ordering = ['code']
This works fine, I get a nice multi-select and everything is perfect. However, I would like to have the possibility to edit the relationship from the side of the SomeCategory table, so I can choose which SomeClass elements are linked to a certain SomeCategory:
class SomeCategoryUpdate(UpdateView):
model = SomeCategory
template_name = 'admin/edit_category.html'
fields = ['code', 'name', ??????? ]
ordering = ['code']
I have tried adding the related_name attribute to the SomeCategory model, but that did not work.
Any ideas if this can be done without using a custom ModelForm?
Key library versions:
Django==1.11.8
psycopg2==2.7.4
PS: this is my very first question asked on stackoverflow, so please let me know if my post is missing any mandatory elements.
Your issue is in the models.py file. You have two classes, but only one of them mentions the other one. You would think that this should be enough since you are using ManyToManyField after all and assume that it would automatically create every connection leading both ways... Unfortunately this is not true. On the database level it does indeed create a separate intermediary table with references to objects in both original tables, but that doesn't mean that both of them will be automatically visible in Django Admin or similar.
If you would attempt to simply create another someclass = models.ManyToManyField(SomeClass) in the SomeCategory class that would fail. Django would try to create another separate intermediary table through which the connection between two main tables is established. But because the name of the intermediary table depends on where you define the ManyToManyField connection, the second table would be created with a different name and everything would just logically collapse (two tables having two separate default ways to have a ManyToMany connection makes no sense).
The solution is to add a ManyToManyField connection to SomeCategory while also referencing that intermediary/through table that was originally created in the SomeClass class.
A couple of notes about Django/python/naming/programming conventions:
Use the name of the table you are referencing to, as the name of the field that is containing the info about that connection. Meaning that SomeClass's field with a link to SomeCategory should be named somecategory instead of allowed_categories.
If the connection is one-to-many - use singular form; if the connection is many-to-many - use plural. Meaning that in this case we should use plural and use somecategories instead of somecategory.
Django can automatically pluralize names, but it does it badly - it simply adds s letter to the end. Mouse -> Mouses, Category -> Categorys. In those kind of cases you have to help it by defining the verbose_name_plural in the special Meta class.
Using references to other classes without extra 's works only if the the class was already defined previously in the code. In the case of two classes referring to each other that is true only one way. The solution is to put the name of the referred class in the quotation marks like 'SomeCategory' instead of SomeCategory. This sort of reference, called a lazy relationship, can be useful when resolving circular import dependencies between two applications. And since by default it's better to keep the style the same and to avoid unnecessary brain energy wasting of "I will decide whether or not to use quotation marks depending on the order the classes have been organized; I will have to redo this quotation marks thingie every time I decide to move some code pieces around" I recommend that you simply use quotation marks every time. Just like when learning to drive a car - it's better to learn to always use turn signals instead of first looking around and making a separate decision of whether someone would benefit from that information.
"Stringifying" (lazy loading) model/class/table name is easy - just add 's around. You would think that stringifying the "through" table reference would work the same easy way. And you would be wrong - it will give you the ValueError: Invalid model reference. String model references must be of the form 'app_label.ModelName'. error. In order to reference the stringified "through" table you need to: (a) add 's around; (b) replace all dots (.) with underscores (_); (c) delete the reference to through!.. So SomeClass.somecategories.through becomes 'SomeClass_somecategories'.
Therefore the solution is this:
class SomeCategory(models.Model):
code = models.CharField(max_length=5)
name = models.CharField(max_length=40)
someclasses = models.ManyToManyField('SomeClass', through='SomeClass_somecategories', blank=True)
class Meta:
verbose_name_plural = 'SomeCategories'
class SomeClass(models.Model):
code = models.CharField(max_length=3, unique=True)
name = models.CharField(max_length=30, unique=False)
age = models.IntegerField(null=False)
somecategories = models.ManyToManyField('SomeCategory')
After this it should be obvious what kind of final changes to make to your UpdateView classes.
You can achieve this in the view and form, without having to specify the additional ManytoMany connections in the
models, using something like the following:
In the View
class SomeClassUpdate(UpdateView):
model = SomeClass
form_class = SomeClassUpdateForm # to specify the form
template_name = 'admin/edit_class.html'
def form_valid(self, form, *args, **kwargs):
initial_somecategorys = SomeCategory.objects.filter(allowed_categories__pk=form.instance.pk)
amended_somecategorys = form.cleaned_data['allowed_categroies']
remove = [x for x in initial_somecategorys if x not in amended_somecategorys]
add = [x for x in amended_somecategorys if x not in initial_somecategorys]
for somecategory in add:
somecategory.allowed_categories.add(form.instance)
somecategory.save()
for somecategory in remove:
somecategory.allowed_categories.remove(form.instance)
somecategory.save()
return super().form_valid(form)
In the Form
The init method at the top pre-populates the form with entries saved on the model.
class SomeClassUpdateForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(SomeClassUpdateForm, self).__init__(*args, **kwargs)
try:
obj = kwargs['instance']
self.fields["some_categories"].initial = SomeCategory.objects.filter(allowed_categories__pk=form.instance.pk)
except (AttributeError, KeyError): # to catch NoneType if new entry being created.
pass
some_categories = forms.ModelMultipleChoiceField(
required=False,
queryset=SomeCategory.objects.all(),
)
class Meta:
model = SomeClass
fields = [
'some_categories'
..etc
]
This should work. I've writen similar code in one of my projects, and it's working fine. However, I don't know if it's
structurally best to use methods like this and not alter the model relationships or whether it's preferable to
alter the model relationships as outlined in other replies. So I'd be interested to know other peoples views on what
the best approach is.
I have some models that represents some companies and their structure. Also all models can generate some Notifications (Notes). User can see own Notes, and, of course, can't see others.
class Note(models.Model):
text = models.CharField(...)
class Company(models.Model):
user = models.ForeignKey(User)
note = models.ManyToManyField(Note, blank='True', null='True')
class Department(models.Model):
company = models.ForeignKey(Company)
note = models.ManyToManyField(Note, blank='True', null='True')
class Worker(models.Model):
department = models.ForeignKey(Department)
note = models.ManyToManyField(Note, blank='True', null='True')
class Document(models.Model)
company = models.ForeignKey(Company)
note = models.ManyToManyField(Note, blank='True', null='True')
The question is how I can collect all Notes for particular user to show them?
I can do:
Note.objects.filter(worker__company__user=2)
But its only for Notes that was generated by Workers. What about another? I can try hardcoded all existing models, but if do so dozen of kittens will die!
I also tried to use backward lookups but got "do not support nested lookups". May be I did something wrong.
EDIT:
As I mentioned above I know how to do this by enumerating all models (Company, Worker, etc. ). But if I will create a new model (in another App for example) that also can generate Notes, I have to change code in the View in another App, and that's not good.
You can get the Notes of a user by using the following query:
For example let us think that a user's id is 1 and we want to keep it in variable x so that we can use it in query. So the code will be like this:
>>x = 1
>>Note.objects.filter(Q(**{'%s_id' % 'worker__department__company__user' : x})|Q(**{'%s_id' % 'document__company__user' : x})|Q(**{'%s_id' % 'company__user' : x})|Q(**{'%s_id' % 'department__company__user' : x})).distinct()
Here I am running OR operation using Q and distinct() at the end of the query to remove duplicates.
EDIT:
As I mentioned above I know how to do this by enumerating all models
(Company, Worker, etc. ). But if I will create a new model (in another
App for example) that also can generate Notes, I have to change code
in the View in another App, and that's not good.
In my opinion, if you write another model, how are you suppose to get the notes from that model without adding new query? Here each class (ie. Department, Worker) are separately connected to Company and each of the classes has its own m2m relation with Note and there is no straight connection to User with Note's of other classes(except Company). Another way could be using through but for that you have change the existing model definitions.
Another Solution:
As you have mentioned in comments, you are willing to change the model structure if it makes your query easier, then you can try the following solution:
class BaseModel(models.Model):
user = models.Foreignkey(User)
note = models.ManyToManyField(Note)
reports_to = models.ForeignKey('self', null=True, default=None)
class Company(BaseModel):
class Meta:
proxy = True
class Document(BaseModel):
class Meta:
proxy = True
#And so on.....
Advantages: No need to create separate table for document/company etc.
object creation:
>>c= Company.objects.create(user_id=1)
>>c.note.add(Note.objects.create(text='Hello'))
>>d = Document.objects.create(user_id=1, related_to=c)
>>d.note.add(Note.objects.create(text='Hello World'))
I have a certain number ob Subjects and Users. An user can vote for one or more subjects. I'd like to make a view displaying the top 10 Subjects sorted by count of votes in descanding order.
In fact, I found a working way, but I'm asking myself if there is more suitable way to do that.
I started with models:
class Subject(models.Model):
text = models.TextField()
class User(models.Model):
name = models.CharField(max_length=64)
subject = models.ManyToManyField(Subject)
now I can type:
Subject.user_set.all()
and I will get those users who has voted for the subject.
Now I would like to make a list view, where top 10 subjects would be displayed sorted by the number of votes.
So I added a class method to the Subject class:
class Subject(models.Model):
text = models.TextField()
#staticmethod
def by_votes():
myList = list(Subject.objects.all())
return sorted(myList, key=lambda s: s.user_set.count(), reverse=True)[:10]
and defined that class based view:
class SubjectListView(ListView):
model = Subject
template_name = "subject_list.html"
context_object_name = "subject_list"
queryset = Subject.by_votes()
Which actually works well.
But I saw in the docs Manager and QuerySet classes, unfortunately I did not quite understand how to use them (defining custom ones) to get what I'm looking for (without dealing with raw SQL queries).
I'm little bit afraid, because I used a list I could run into memory problems having large number of subjects.
What would you say, would it be more suitable to use custom Manager oder QuerySet for that, wouldn't?
If yes, how to do it?
Any other alternative ideas?
Thanks!
Peter
No need for custom managers or raw SQL here, just aggregation.
from django.db.models import Count
Subject.objects.annotate(user_count=Count('user').order_by('-user_count')[:10]
I'm fairly new to this but I solved a similar problem with this:
Subject.objects.all().annotate(count=Count('user')).order_by('-count')[:10]
Aggregation Doc
Let's say I have two models looking like this:
class ProductType(models.Model):
product_type = models.CharField(max_length=100)
class Product(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField()
product_type = models.ForeignKey(ProductType)
score = models.PositiveIntegerField(default=0)
Now I want to fetch the top two products (the two with highest score) from each ProductType.
So if I have Phones, Computers, TVs as ProductTypes I want the two best Phones, Computers, TVs.
As I don't even know how to do this in MySQL I've tried searching for the MySQL-solution but the ones I find is extremely complex and this doesn't feel like an extremely complex thing to do.
I am leaning towards making an own model for the top products and have a cronjob to fix this, but I'd like to see if there's an easier solution to this.
Well, you could make a method inside of the ProductType class that returns the top two results for its products:
class ProductType(models.Model):
product_type = models.CharField(max_length=100)
def TopTwoProducts(self):
return self.product_set.order_by('-score')[0:2]
Then you would just do something like:
for type in ProductType.objects.all():
type.TopTwoProducts()
While adam's solution is correct, a more django-ish way would be to use a manager.
See Managers versus class methods on James Bennett's blog
Among other advantages :
a manager carries all query-specific code, while avoiding to clutter the model class
the same manager class can be shared among several classes
the manager can be used directly on a model class, or via a one-to-many or m2m relation
Thus, for the above question :
from django.db import models
from django.db.models.manager import Manager
class ProductScoreManager(Manager):
use_for_related_field = True
def top_score(self, limit=2):
return self.get_query_set().order_by('-score')[:limit]
Then add this manager class as default manager for Product :
class Product(models.Model):
...
objects = ProductScoreManager()
...
Now, thanks to objects overriding the default manager, and use_for_related_field allowing its use in relation queries, the top_score method can be used in any model related to products.
myproducttype = ProductType.objects.get(...)
myproducttype.products.top_score() # return top 2 products
This allows a more consistent syntax : the related products is always accessed via products, top_score acting as a filter. Additionally, ProductType class is no more cluttered with Product's query logic.
Just filter out product type from Product model slice them like this -
product_type_list = ProductType.objects.value("id")
for product_type in product_type_list:
Product.objects.filter(
product_type=product_type
).order_by("-score")[0:2]