How to create form field for multiple models in Django? - python

Tried to figure this out on my own but stumped -
I'm working on a crm project to learn Django and have gotten stuck trying to incorporate activities between a user and client. Specifically, I'm trying to make it possible to record an email interaction and to have the from/to fields reference either a user or client model. So essentially an email can be recorded as either from a user to client or vice versa. The next part would be to allow for multiple clients or users to be tagged in the correct fields of this interaction.
I've tried incorporating the to and from fields as models so that they can use the GenericForeignKey class like so:
class Activity(models.Model):
owner = models.ForeignKey(User, on_delete=models.DO_NOTHING)
date = models.DateTimeField()
class EmailTo(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.DO_NOTHING)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type')
class EmailFrom(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.DO_NOTHING)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type')
class EmailActivity(Activity):
emailto = models.ForeignKey(EmailTo, on_delete=models.DO_NOTHING)
emailfrom = models.ForeignKey(EmailFrom, on_delete=models.DO_NOTHING)
body = models.TextField(blank=True)
but now I'm stuck trying to figure out how to represent that on a form. I thought maybe I could use a union to combine two queries into one field using a ModelMultipleChoiceField:
class EmailActivityForm(forms.ModelForm):
emailto = forms.ModelMultipleChoiceField(
queryset=Client.objects.all().union(User.objects.all()),
label="To")
emailfrom = forms.ModelMultipleChoiceField(
queryset= Client.objects.all().union(User.objects.all()),
label="From")
body = forms.CharField(widget=forms.Textarea)
class Meta:
model = EmailActivity
exclude = '__all__'
but see now that this is not possible since the queries are not the same size.
I'm starting to think I need to go back to my user models and make users and clients inherit from one "Person" model or something similar. Wanted to check here first though to see if I was possibly missing something.

you better write your own manual form
class EmailActivityForm(forms.Form):
emailto = forms.ModelMultipleChoiceField(queryset=Client.objects.all().union(User.objects.all()), label="To")
emailfrom = forms.ModelMultipleChoiceField(queryset= Client.objects.all().union(User.objects.all()), label="From")
body = forms.CharField(widget=forms.Textarea)
and keep going with all the other fields left.
by the way, the forms.ModelForms is made for a single model, not multiple

Related

Django - How to create dependent selects

My task is to implement a form in which the choice of the value of the second field depends on the value of the first field. (For example, if the value of the first field is Cars, then the second field should show sedan/SUV, etc., if the value of the first field is Commercial vehicles, then the second box should show truck/bus, etc.)
Code models.py:
class TypeTransport(models.Model):
transport_name = models.CharField(max_length=100, verbose_name='kind of transport')
class TypeBodyTransport(models.Model):
transport = models.ForeignKey(TypeTransport, on_delete=models.CASCADE, blank=True, null=True,
verbose_name='kind of transport')
body_name = models.CharField(max_length=100, verbose_name='transport body type')
class Advertisement(models.Model):
transport = models.ForeignKey(TypeTransport, on_delete=models.SET_NULL, blank=True, null=True,
verbose_name='kind of transport')
body = models.ForeignKey(TypeBodyTransport, on_delete=models.SET_NULL, blank=True, null=True,
verbose_name='transport body type ')
Code forms.py:
class CreateAdvertisementForm(forms.ModelForm):
transport = forms.ModelChoiceField(queryset=TypeTransport.objects.all(), to_field_name="transport_name")
body = forms.ModelChoiceField(queryset=TypeBodyTransport.objects.filter(transport=transport),
to_field_name="body_name")
class Meta:
model = Advertisement
fields = ('transport', 'body')
I thought it could be done with filter(transport=transport), but this error is returned: TypeError: Field 'id' expected a number but got <django.forms.models.ModelChoiceField object at 0x7f40d7af5ac0>.
Can you please tell me how to implement the feature I need?
have you tried:
class CreateAdvertisementForm(forms.ModelForm):
transport = forms.ModelChoiceField(queryset=TypeTransport.objects.all(), to_field_name="transport_name")
body = forms.ModelChoiceField(queryset=TypeBodyTransport.objects.filter(transport=transport.id),
to_field_name="body_name")
class Meta:
model = Advertisement
fields = ('transport', 'body')
instead of transport = transport, try transport = transport.id
I solved this problem using the django-smart-selects library. Additionally I can say that in the forms it is necessary to remove field which references to the ModelChoiceField, because it interferes with its queryset parameter. I'm still a beginner, so I didn't immediately guess that the problem was in chained selects (I edited the question).

What's the best way to create a generic serializer which is able to serialize all the subclasses of the base model in Django Rest Framework?

I'm currently working on a notification system which is going to deliver many different types of notifications through an API.
I have a base notification class called BaseNotification. Here is the code:
class BaseNotification(models.Model):
message = models.CharField(max_length=500)
seen = models.BooleanField(default=True)
class Meta:
abstract = True
And I want to subclass this model to create different variations of notifications:
class InvitationNotification(BaseNotification):
invited_user = models.ForeignKey(User, on_delete=models.CASCADE)
campagin = models.ForeignKey(Campagin, on_delete=models.CASCADE)
# ... the rest of specific fields
class ChatMessageNotification(BaseNotification):
contact = models.ForeignKey(User, on_delete=models.CASCADE)
chat_room = models.ForeignKey(SomeSortOfChatRoomWhichIsNotImplementedYet, on_delete=models.CASCADE)
# ... the rest of specific fields
As you can see, each of those variations, has some meta-data associated with it. The front-end developer would be able to create user interactions using these meta data (for example, in the case of a chat message, the front-end developer would redirect the user to the chat room)
I want to list all of these notifications through one unified API. For that, I need a serializer to serialize the objects as json but I don't want to have a sperate serializer for each. Ideally I want to have a generalized serializer which is capable of serializing all kinds of notifications and generates different json output depending on the type of the notification object passed in. (It might use other serializers under the hood).
Maybe I'm on the wrong track but the end goal is to deliver notifications and all their meta-data through one unified API.
I really need your suggestions and help. Thank you all in advance.
I finally figured it out by myself. My end goal was to provide some metadata for the front-end developer so that he can implement user interactions based on the type of the notification.
I simply solved the problem with a JSONField. That works just fine and that was the only thing I needed.
models.py
class Notification(models.Model):
NEW_MEMBER_JOINED = 'NEW_MEMBER_JOINED'
INVITATION = 'INVITATION'
TEXT = 'TEXT'
NEW_POST_CREATED = 'NEW_POST_CREATED'
NEW_MESSAGE = 'NEW_MESSAGE'
NEW_CAMPAIGN_CONTENT = 'NEW_CAMPAIGN_CONTENT'
NEW_COMMENT = 'NEW_COMMENT'
target = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications')
text = models.CharField(max_length=300)
type = models.CharField(max_length=255)
date_created = models.DateTimeField(auto_now_add=True)
seen = models.BooleanField(default=False)
metadata = models.JSONField(null=True, blank=True)
def __str__(self) -> str:
return f"[{self.type}] {self.text}"
serializers.py
class NotificationSerializer(serializers.ModelSerializer):
class Meta:
model = Notification
fields = '__all__'
read_only_fields = [
'text', 'type', 'metadata', 'date_created', 'seen'
]
In my case, this solution works the best.

How to filter a one-to-one generic relation with Django?

I have a moderation model :
class ItemModeration(models.Model):
class Meta:
indexes = [
models.Index(fields=['object_id', 'content_type']),
]
unique_together = ('content_type', 'object_id')
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
item = GenericForeignKey('content_type', 'object_id')
published = models.BooleanField(default=False)
...
A descriptor to attach a moderation object on-the-fly :
class ItemModerationDescriptor(object):
def __init__(self, **default_kwargs):
self.default_kwargs = default_kwargs
def __get__(self, instance, owner):
ctype = ContentType.objects.get_for_model(instance.__class__)
try:
moderation = ItemModeration.objects.get(content_type__pk=ctype.id,
object_id=instance.pk)
except ItemModeration.DoesNotExist:
moderation = ItemModeration(item=instance,**self.default_kwargs)
moderation.save()
return moderation
And a model I want to moderate :
class Product(models.Model):
user = models.ForeignKey(
User,
null=True,
on_delete=models.SET_NULL)
created = models.DateTimeField(
auto_now_add=True,
blank=True, null=True,
)
modified = models.DateTimeField(
auto_now=True,
blank=True, null=True,
)
name = models.CharField(
max_length=PRODUCT_NAME_MAX_LENGTH,
blank=True, null=True,
)
moderation = ItemModerationDescriptor()
Now I can see a product 'published' state easily :
p=Product(name='my super product')
p.save()
print(p.moderation.published)
-> False
The generic relation is useful because I will be able to search the objects to moderate whatever the type is : it could be products, images, comments.
to_moderate_qs = ItemModeration.objects.filter(published=False)
Now, how can I get a filtered list of published products ?
I would like to do something like this
published_products_qs = Product.objects.filter(moderation__published=True, name__icontains='sony')
But, of course, it won't work as moderation attribute is not a Django model field.
How can I do that efficiently ? I am thinking a about an appropriate JOIN, but I cannot see how to do that with django without using raw SQL.
Django has a great built in answer for this: the GenericRelation. Instead of your descriptor, just define a generic relation on your Product model and use it as a normal related field:
from django.contrib.contenttypes.fields import GenericRelation
class Product(models.Model):
...
moderation = GenericRelation(ItemModeration)
Then handle creation as you normally would with a related model, and filtering should work exactly as you stipulated. To work as your current system, you'd have to put in a hook or save method to create the related ItemModeration object when creating a new Product, but that's no different from other related django models. If you really want to keep the descriptor class, you can obviously make use of a secondary model field for the GenericRelation.
You can also add related_query_name to allow filtering the ItemModeration objects based only on the Product content type.
WARNING if you do use a GenericRelation note that it has a fixed cascading delete behavior. So if you don't want ItemModeration object to be deleted when you delete the Product, be careful to add a pre_delete hook or equivalent!
Update
I unintentionally ignored the OneToOne aspect of the question because the GenericForeignKey is a one-to-many relation, but similar functionality can be effected via smart use of QuerySets. It's true, you don't have access to product.moderation as a single object. But, for example, the following query iterates over a filtered list of products and extracts their name, the user's username, and the published date of the related ModerationItem:
Product.objects.filter(...).values_list(
'name', 'user__username', 'moderation__published'
)
You'll have to use the content_type to query the table by specific model type.
like this:
product_type = ContentType.objects.get_for_model(Product)
unpublished_products = ItemModeration.objects.filter(content_type__pk=product_type.id, published=False)
For more details on the topic check contenttypes doc

Get site-specific user profile fields from user-created object

I am using Django sites framework (Django 2.1) to split an app into multiple sites. All of my models except the User model are site-specific. Here is my Post model:
post.py
class Post(models.Model):
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
related_name='children',
related_query_name='child',
blank=True,
null=True,
)
title = models.CharField(
max_length=255,
blank=True,
)
body_raw = models.TextField()
body_html = models.TextField(blank=True)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
site = models.ForeignKey(Site, on_delete=models.CASCADE)
on_site = CurrentSiteManager()
I have no problem separating posts out by site. When I want to get the posts, I call:
posts = Post.on_site.filter(...)
I have a separate model called UserProfile. It is a many-to-one profile where there is a unique profile created for each user-site combination (similar to profile implementation at SE). The profile has a reputation attribute that I want to access when I get any post. This reputation attribute should be different for each site (like how on SE you have different rep on each site you are a member of).
user_profile.py
class UserProfile(models.Model):
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
reputation = models.PositiveIntegerField(default=1)
site = models.ForeignKey(Site, on_delete=models.CASCADE)
on_site = CurrentSiteManager()
How do I access the user's username (on the User model) as well as the user's reputation (on the UserProfile model) when I get Posts from a query?
I'd like to do something like:
Post.on_site.filter(...)
.select_related('user__userprofile')
.filter_related(user__userprofile.site == get_current_site())
How do I filter a Many-To-One related model?
Better to make UserProfile -> User relationship to be OnetoOne,
because Django doesn't know which of many profiles to show
(but you also need to define related_name)
models.OneToOneField(get_user_model(), related_name='userprofile_rev')
Then you will be able to do this
qs = Post.on_site.filer().select_related('user', 'user__userprofile_rev')
for post in qs:
print(post.user.username, post.user.userprofile_rev.reputation)
If you don't want to change your DB structure you can do like this
(but you need to specify which profile to return)
qs = Post.on_site.filer().select_related('user').prefetch_related('user__userprofile_set')
for post in qs:
print(post.user.username, post.user.userprofile_set[0].reputation)

Correct Django models relationship

Preface: I have two models (Product and UserProfile) and I would to implement a mine comments system. The comment is related to an object, Product or UserProfile.
class Product(models.Model):
name = models.CharField(max_length = 40)
comments = models.ManyToMany(Comment)
class UserProfile(models.Model):
user = models.ForeignKey(User, unique = True)
comments = models.ManyToMany(Comment)
class Comment(models.Model):
text = models.TextField()
author = models.ForeignKey(User)
timestamp = models.DateTimeField(auto_now_add = True)
Is it correct the logic under these models? I'm doubtful, because this way means a Product can has many comments (and it's correct) but also a Comment can has many products (I don't think it's correct).
It isn't?
Your comment should have have a ForeignKey to the UserProfile or Product i.e. A single comment can only belong to a single product or user profile, but a user profile/product can have many different comments
def Comment(models.Model):
profile = models.ForeignKey(UserProfile)
product = models.ForeignKey(Profile)
Obviously this isn't ideal as there are two relationships you need to manage, and sometimes you will only want to use one etc.
To get over this, you can use a Generic Foreign Key:
https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#generic-relations
this allows you to link a comment to any type of content (products, user profiles and more) without having to specify the models up front
def Comment(models.Model):
...
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')

Categories

Resources