Django: GenericForeignKey and unique_together - python

In the application I'm working on I'm trying to share access tokens within a company. Example: a local office can use the headquarter's tokens to post something on their Facebook page.
class AccessToken(models.Model):
"""Abstract class for Access tokens."""
owner = models.ForeignKey('publish.Publisher')
socialMediaChannel = models.IntegerField(
choices=socialMediaChannelList, null=False, blank=False
)
lastUpdate = models.DateField(auto_now=True)
class Meta:
abstract = True
Since Facebook, Twitter and other social media sites handle access tokens in their own way I made and abstract class AccessToken. Each site gets its own class e.g.
class FacebookAccessToken(AccessToken):
# class stuff
After doing some reading I found out that I must use a GenericForeignKey to point to classes that inherit AccessToken. I made the following class:
class ShareAccessToken(models.Model):
"""Share access tokens with other publishers."""
sharedWith = models.ForeignKey('publish.Publisher')
sharedBy = models.ForeignKey(User)
# for foreignkey to abstract model's children
contentType = models.ForeignKey(ContentType)
objectId = models.PositiveIntegerField()
contentObject = GenericForeignKey('contentType', 'objectId')
class Meta:
unique_together = (('contentObject', 'sharedWith'))
When I run the django test server I get the following error:
core.ShareAccessToken: (models.E016) 'unique_together' refers to field
'contentObject' which is not local to model 'ShareAccessToken'. HINT:
This issue may be caused by multi-table inheritance.
I don't understand why I get this error, first time using GenericForeignKey. What am I doing wrong?
If there is a smarter way to share the access tokens I would love to hear about it.

Your use of the generic foreign key in this situation is correct.
The error is coming from your unique_together declaration in your model. unique_together can only be used with columns that exist in the database. Since contentObject is not a real column, Django complains about the constraint.
Instead, you can do the following:
unique_together = (('contentType', 'contentId', 'sharedWidth'),)
This is equivalent to what you had defined in your question because contentObject is really just the combination of contentType and contentId behind the scenes.

Related

How to change field in ModelForm generated html form?

I'm making one of my first django apps with sqlite database. I have some models like for example:
class Connection(models.Model):
routeID = models.ForeignKey(Route, on_delete=models.CASCADE)
activityStatus = models.BooleanField()
car = models.ForeignKey(Car, on_delete=models.CASCADE)
class Route(models.Model):
name = models.CharField(max_length=20)
and forms
class RouteForm(ModelForm):
class Meta:
model = Route
fields = ['name']
class ConnectionForm(ModelForm):
class Meta:
model = Connection
fields = ['routeID', 'activityStatus', 'car']
And in my website, in the url for adding new Connection, I have cascade list containing RouteIDs. And I'd like it to contain RouteName, not ID, so it would be easier to choose. How should I change my ConnectionForm, so I could still use foreign key to Route table, but see RouteName instead of RouteID?
For now it's looking like this, but I'd love to have list of RouteNames, while still adding to Connection table good foreign key, RouteID
Update the Route Model's __str__ method:
class Route(models.Model):
name = models.CharField(max_length=20)
def __str__(self):
return self.name
Because the __str__() method is called whenever you call str() on an object. Django uses str(obj) in a number of places like in Modelform. By default it returns id or pk that is why you were seeing ids in model form. So by overriding it with name, you will see the names appear in choice field. Please see the documentation for more details on this.

How to make a django model "commentable", "likeable" and "rateable"

I am using Django 2.0.8 and Python 3.5 for a project. I have different models in my project, some of which, I want to allow commenting on - with both the object (e.g. a blogpost) and comments to the blogpost being likeable.
I am using the threaded comments django app to provide commenting functionality.
Assuming I have a model Foo (see below):
from django.db import models
from django.conf import settings
class Foo(models.Model):
owner = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False, default=1, on_delete = models.PROTECT)
# ...
class Likeable():
pass
class Rateable():
pass
How could I use mixins (or any other mechanism for that matter), to make the object Foo "commentable" (i.e. an object which can be commented upon), "likeable" (i.e. an object which can be commented upon) and "rateable" (i.e. an object which can be rated?)- bearing in mind that comments on an objects may be BOTH liked and rated.
According to django documentation , you can achieve this using the Content types Framework. ContentType is a generic model that permits you to track all the models included in INSTALLED_APPS using for that their app_label, model_name and pk. The way it works is easy:
Your generic Comment model
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.conf import settings
class Comment(models.Model):
# Generic relation fields
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
# Model specific fields
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)
comment = models.TextField()
created = models.DatetimeField(auto_now_add=True)
# ...
Your reusable generic relation model. The best way is using abstract model classes or mixins. For example, using abstract models:
from django.db import models
from django.contrib.contenttypes.fields import GenericRelation
class Commentable(models.Model):
comments = GenericRelation(Comment)
class Meta:
abstract = True
Your Commentable model:
from django.db import models
class Foo(Commentable, Likeable, ...):
# your stuff
How to use it:
# Add a new comment to Foo
foo = new Foo()
foo.save()
foo.comments.create(author=author, comment="Your comment")
# Retrieve all comments from an specific user no matter the base model
comments = Comment.objects.filter(author=author)
EDIT As #ozren1983 said, each approach has its own downsides, but this is the standard way to do it.
The main advantages are:
You can retrieve all the comments (for example) made in all your commentable models in just one query. Using the approach of having a comment, like, etc table per model, you would need to concatenate a query per model. This could be problematic and a bit challenging if you have a lot of models or if you want to merge the results and order them, for example.
Just one table per functionality (comments, likes) implies just one database migration in case of change. This could be key if your database is huge.
The main disadvantage is the lack of integrity checks of this generic relationship in database. But if you plan to use the django ORM strictly, nothing should be broken.
BONUS: Another approach that many projects use is inheriting the models (one to one relationship) from an specific one called Item or Thread. Then, you can add all the comments, likes, etc functionalities to this model. This is called multi-table inheritance. An example:
from django.db import models
class Thread(models.Model):
pass
class Comment(models.Model):
# Relation with thread
thread = models.ForeignKey(
Thread,
on_delete=models.CASCADE,
related_name="comments"
)
# Model specific fields
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)
comment = models.TextField()
created = models.DatetimeField(auto_now_add=True)
# ...
class Foo(Thread):
pass
Unlike using the generic relationships, the main advantage of this method is that, this way, you have database integrity checks.
The main disadvantage is that your database structure could become complex.
Based on my experience and recommendations in Two scoops of Django, I would advise against using GenericForeignKey and GenericRelation. Two big downsides of that approach are:
slow queries
danger of data corruption
Instead, I would use following approach. Let's say you have 3 models:
class User(models.Model):
username = models.CharField(max_length=255)
class Author(models.Model):
name = models.CharField(max_length=255)
class Post(models.Model):
title = models.CharField(max_length=255)
author = models.ForeignKey(Author)
Add abstract Like model, and use it as base class for other models that will implement liking functionality.
class Like(models.Model):
user = models.ForeignKey(User)
date_created = models.DateTimeField(auto_now_add=True)
class Meta:
abstract = True
class AuthorLike(Like):
author = models.ForeignKey(Author)
class PostLike(Like):
post = models.ForeignKey(Post)
Similarly, add abstract Rating model and use it as a base class:
class Rating(models.Model):
user = models.ForeignKey(User)
rate = models.PositiveSmallIntegerField()
date_created = models.DateTimeField(auto_now_add=True)
class Meta:
abstract = True
class AuthorRating(Rating):
author = models.ForeignKey(Author)
class PostRating(Rating):
post = models.ForeignKey(Post)
You can use same approach to enable liking and rating to the Comments model you are using:
from threadedcomments.models import ThreadedComment
class ThreadedCommentRating(Rating):
threadedcomment = models.ForeignKey(ThreadedComment)
class ThreadedCommentLike(Like):
threadedcomment = models.ForeignKey(ThreadedComment)
The django-contrib-comments app, according to documentation, makes use of GenericForeignKey, meaning its own model can create a relation to any other model in your project.
A simple solution would be to just copy that existing functionality, creating your own Like/Rate application based on the same concept (i.e. storing the Like/Rate models in that application's models).
I think you would get very far starting out by forking the https://github.com/django/django-contrib-comments codebase.
(I assume you have searched and failed to find an already existing application that already does this).

Set a custom queryset (select_related) for a list field in the admin change page?

I have an using the Django admin interface to manage a lot of objects, and one of the page is giving me issue, this page has a field to a related object (Foreign Key) that has a __str__ that also goes to its related objects, this make a lot of queries and is barely useable (Around 3000 queries to show the page as there are a LOT of objects).
I would like to know if there is a way to set a custom queryset ? I would like to add a select_related or prefetch_related to this element.
The part causing issue is this certificate requests list :
The page model (Certificate has the following attribute:
class Certificate(models.Model):
certificate_request = models.OneToOneField(
"CertificateRequest",
verbose_name=_("Certificate request"),
related_name="certificate",
blank=True,
null=True
)
And the related model has this :
class CertificateRequest(models.Model):
domain = models.ForeignKey(
"Domain",
verbose_name=_("Domain"),
related_name="certificate_requests"
)
def __str__(self):
return "{state} certificate request for {domain} from {creation_date}".format(
state=dict(self.STATUS).get(self.status),
domain=self.domain.fqdn,
creation_date=self.creation_date
)
What would be the way to fix this ? How can I set a queryset on this part ?
EDIT: I added more informations.
I tried using a custom form, but this didn't do any change :
class CertificateForm(forms.ModelForm):
certificate_request = forms.ModelChoiceField(queryset=CertificateRequest.objects.select_related("domain"))
class Meta:
model = Certificate
fields = "__all__"
#admin.register(Certificate)
class CertificateAdmin(CompareVersionAdmin):
model = Certificate
class Meta:
form = CertificateForm
You can create a custom ModelForm for your admin where you specify a ModelChoiceField for the ForeignKey. Here you can specify the queryset parameter:
# forms.py
class MyForm(forms.ModelForm):
certificate_request = forms.ModelChoiceField(queryset=CertReq.objects.foo().bar())
# select/prefetch-------^^^^^^^^^^^
class Meta:
model = Foo
# admin.py
class YourAdmin(ModelAdmin):
form = MyForm
The get_object method on the ModelAdmin class is what is responsible for retrieving the object to edit. You could certainly extend that method in your subclass to use select_related as necessary.

Defining the name of a ManyToOne relationship in Django

I would like to define a django model which has many-to-one relationship with itself. It is a user profile, connected as a OneToOne field with the authentication user model. I would like to save which user (if any) was the one who referred the 'current' user to my system. This means I have the following definition:
class UserProfile(models.Model):
user = models.OneToOneField(User, blank=True, related_name='profile')
class Meta:
abstract = True
class SpecificUserProfile(UserProfile):
referrer = models.ForeignKey('self')
I saw the django defaults to naming the set of referenced models by the name of the class with a suffix _set. I believe I will be getting something along the lines of specific_user_profile_set. I would much prefer to have it named u1.referrer and u2.referred or u2.referred_set. Is there any way this can be achieved?
related_name='profile'
This is the argument to define a name for any related field, so:
class SpecificUserProfile(UserProfile):
referrer = models.ForeignKey('self', related_name='referred')

Django model creation jointly unique fields

I'm trying to create a model for Django that looks like this:
class Device(Model):
UDID = CharField(length=64, primary_key=True)
# more irrelevant stuff
class DeviceProperty(Model):
device = ForeignKey(Device)
name = CharField(length=255)
value = CharField(length=255)
readOnly = BooleanField()
But then, for data-integrity reasons, a single device shouldn't have two properties with the same name. So I would need to make the device and name fields of DeviceProperty jointly unique.
A simple way to achieve this would be by having a composite primary key on the two fields, but this raises a few issues, and more importantly isn't supported by Django.
I haven't found anything about this in the Django documentation. Have I missed anything?
unique_together is what you want.
class DeviceProperty(Model):
…
class Meta:
unique_together = ['device', 'name']

Categories