Django Inner Join on Derived Query - python

I have two models shown as follows. I want to be able to execute this query through the django ORM, essentially giving me the CustomUser class alongside two derived fields: max(message.sent_at) and max(case when read_at is null then 1 else 0 end). Those two fields would enable me to sort threads of messages by usernames and latest activity.
Here are my classes:
class CustomUser(AbstractBaseUser, PermissionsMixin):
username_validator = UnicodeUsernameValidator()
username = models.CharField(_('username'), max_length=150, unique=True, help_text=_('Required. 150 characters or fewer. Letters, digits and #/./+/-/_ only.'), validators=[username_validator], error_messages={'unique': _('A user with that username already exists.'),},)
email = models.EmailField(_('email address'), blank=True)
first_name = models.CharField(_('first name'), max_length=30, blank=True)
last_name = models.CharField(_('last name'), max_length=150, blank=True)
is_staff = models.BooleanField(_('staff status'), default=False, help_text=_('Designates whether the user can log into this admin site.'),)
is_active = models.BooleanField(_('active'), default=True, help_text=_('Designates whether this user should be treated as active. Unselect this instead of deleting accounts.'),)
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
bio = models.TextField(max_length=500, null=True, blank=True)
location = models.CharField(max_length=30, null=True, blank=True)
birth_date = models.DateField(null=True, blank=True)
phone_number = PhoneNumberField(default='+10000000000')
gender = models.CharField(max_length=32, choices=[(tag.name, tag.value) for tag in GenderChoice], default=GenderChoice.UNSPECIFIED.value)
objects = UserManager()
EMAIL_FIELD = 'email'
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email']
class Meta:
ordering = ['username']
verbose_name = _('user')
verbose_name_plural = _('users')
and
class Message(AbstractIP):
subject = models.CharField(_('Subject'), max_length=120, blank=True)
body = models.TextField(_('Body')) # Do we want to cap length or enforce non-blank?
sender = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='sender_messages', verbose_name=_('Sender'), on_delete=models.CASCADE)
recipient = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='receiver_messages', verbose_name=_('Recipient'), blank=True, on_delete=models.CASCADE)
parent_msg = models.ForeignKey('self', related_name='next_messages', null=True, blank=True, verbose_name=_('Parent message'), on_delete=models.CASCADE)
sent_at = models.DateTimeField(_('sent at'), null=True, blank=True)
read_at = models.DateTimeField(_('read at'), null=True, blank=True)
replied_at = models.DateTimeField(_('replied at'), null=True, blank=True)
sender_deleted_at = models.DateTimeField(_('Sender deleted at'), null=True, blank=True)
recipient_deleted_at = models.DateTimeField(_('Recipient deleted at'), null=True, blank=True)
ip = models.GenericIPAddressField(verbose_name=_('IP'), null=True, blank=True)
user_agent = models.CharField(verbose_name=_('User Agent'), blank=True, max_length=255)
objects = MessageManager() # Manager for Message queries
def new(self):
"""Returns whether the recipient has read the message or not"""
if self.read_at is not None:
return False
return True
def replied(self):
"""Returns whether the recipient has written a reply to this message"""
if self.replied_at is not None:
return True
return False
def __str__(self):
if self.subject is not None:
return self.subject
if self.body is not None:
return self.body[:40]
return None
def get_absolute_url(self):
return reverse('messages_detail', args=[self.id])
def save(self, **kwargs):
if not self.id:
self.sent_at = timezone.now()
super(Message, self).save(**kwargs)
class Meta:
ordering = ['-sent_at']
verbose_name = _('Message')
verbose_name_plural = _('Messages')
The query I want to be able to perform equates to this, but I cannot figure out how to do it in the ORM, where %s is a placeholder for the CustomUser.id (pk) field of a given user.
SELECT webrtc_customuser.*
,MAX(webrtc_message.sent_at) AS sent_at
,MAX(CASE WHEN webrtc_message.read_at IS NULL AND webrtc_customuser.id <> webrtc_message.sender_id THEN 1 ELSE 0 END) AS has_unread
FROM webrtc_customuser
INNER JOIN webrtc_message
ON (
webrtc_customuser.id = webrtc_message.sender_id
AND webrtc_message.sender_id = %s
AND webrtc_message.sender_deleted_at IS NULL
) OR (
webrtc_customuser.id = webrtc_message.recipient_id
AND webrtc_message.recipient_id = %s
AND webrtc_message.recipient_deleted_at IS NULL
)
I managed to get the correct user_id and derived fields with the following queries but cannot figure out how to get the CustomUser properties joined alongside them.
messages = self.values(
user_fk=Case(When(sender=user, then='recipient'), default='sender', output_field=models.IntegerField())
).exclude(
sender=user, recipient=user
).filter(
Q(sender=user, sender_deleted_at__isnull=True) |
Q(recipient=user, recipient_deleted_at__isnull=True)
).annotate(
max_sent_at=Max('sent_at'),
has_unread=Max(Case(When(~Q(sender=user) & Q(read_at__isnull=True), then=1), default=0, output_field=models.IntegerField()))
).order_by()
Thank you in advance for your time!
Edit: updated ORM query

You need to specify the desired user properties individually:
messages = self.values(
user_email=Case(When(sender=user, then='recipient__email'), default='sender__email'),
user_username=Case(When(sender=user, then='recipient__username'), default='sender__username'),
)
Not very pretty, particularly as you have to repeat the CASE statement for every column and may even need to specify an output_field for every one.
To get around that, ie. to get all user properties without selecting them one by one, you'd either need to a) select from CustomUser.object (figuring out how to select the relevant users and get the relevant annotations), or b) select full message objects rather than just a values() dictionary. Then you can access the full user objects via message.senderand message.recipient. But here again, the challenge would be how to filter and annotate the messages queryset using subqueries, since just omitting values() will bust the aggregates in your annotations as every message object will then be unique.

Related

Django is annotating wrong Count (possibly duplicates?)

I have a model ChatMessage that has a field sender which is a ForeignKey to User model.
I'm trying to annotate a number of all the ChatMessage objects that haven't been read (eg. have seen_at__isnull=True).
For a given user, there is only one sent message with seen_at__isnull=True but Django returns 11.
User.objects.select_related(...).annotate(
sent_unread_messages=Count('sent_chat_messages',
filter=Q(sent_chat_messages__seen_at__isnull=True))).get(pk=1234).sent_unread_messages
do you know where is the problem?
EDIT:
class ChatMessageManager(models.Manager):
def get_queryset(self) -> models.QuerySet:
return super().get_queryset().select_related('sender', 'recipient')
def as_sender_or_recipient(self, user) -> models.QuerySet:
return self.get_queryset().filter(Q(sender=user) | Q(recipient=user))
class ChatMessage(BaseModel):
objects = ChatMessageManager()
sender = models.ForeignKey('users.User', verbose_name='Odosielateľ', null=True, blank=True,
on_delete=models.SET_NULL, related_name='sent_chat_messages')
recipient = models.ForeignKey('users.User', verbose_name='Adresát', null=True, blank=True,
on_delete=models.SET_NULL, related_name='received_chat_messages')
content = models.TextField('Obsah')
attachment = models.FileField('Príloha', null=True, blank=True)
attachment_filename = models.CharField('Názov prílohy', null=True, blank=True, max_length=128)
meta = models.JSONField(verbose_name='Meta', null=True, blank=True, help_text='must be JSON')
seen_at = models.DateTimeField('Prečítané o', null=True, blank=True)
class CustomUserManager(UserManager):
def get_queryset(self):
return super().get_queryset().select_related('staff_profile', 'client_profile').annotate(
sent_unread_messages=Count('sent_chat_messages',
filter=Q(sent_chat_messages__seen_at__isnull=True))).annotate(
received_unread_messages=Count('received_chat_messages',
filter=Q(received_chat_messages__seen_at__isnull=True))).annotate(
sent_latest_message=Subquery(
ChatMessage.objects.filter(sender=OuterRef('pk')).order_by('-created').values('content')[:1])).annotate(
sent_latest_message_dt=Subquery(
ChatMessage.objects.filter(sender=OuterRef('pk')).order_by('-created').values('created')[:1])).annotate(
received_latest_message=Subquery(
ChatMessage.objects.filter(recipient=OuterRef('pk')).order_by('-created').values('content')[
:1])).annotate(
received_latest_message_dt=Subquery(
ChatMessage.objects.filter(recipient=OuterRef('pk')).order_by('-created').values('created')[:1]))
can you try using .distinct() method (which removes the duplicate elements from a queryset) when getting the messages ?

How to display only the appointments that the current logged in user has made instead of fetching up all the appointments from the database in Django

This the views.py file.
How can i display the appointments made by the current logged in user?
def user(request):
client = Client.objects.all()
appointments = Appointment.objects.all()
context = {'appointments': appointments, 'client': client,
}
return render(request, 'users/user.html', context)
Here is my Models.py. I need to display the appointments by a user when they are logged in to their profile.
class Appointment(models.Model):
CATEGORY = (
('Plumbing', 'Plumbing'),
('Electrical', 'Electrical'),
('Cleaning', 'Cleaning'),
)
STATUS = (
('Pending', 'Pending'),
('Delivered', 'Delivered'),
)
user = models.ForeignKey(Client, null=True, on_delete=models.SET_NULL)
name = models.CharField(max_length=200, null=True)
worker = models.ForeignKey(Worker, null=True, on_delete=models.SET_NULL)
category = models.CharField(max_length=200, null=True, choices=CATEGORY)
task_date = models.DateField(_("Task Date"), blank=True, null=True)
task_location = models.CharField(max_length=200, null=True)
date_created = models.DateTimeField(auto_now_add=True, null=True)
status = models.CharField(max_length=200, null=True, choices=STATUS)
budget = models.FloatField(null=True)
task_description = models.CharField(max_length=1000, null=True)
task_image = models.ImageField(
null=True, blank=True, help_text='Optional.')
def __str__(self):
return str(self.user)
instead of using all() in your query use filter()
all() gives you all the entries in the table.
do something like this:
appointments = Appointment.objects.filter(user = request.user)
the left side "user" inside the filter must be a column in the Appointment model/table. you can pass multiple parameters inside the filter.
Yea it worked. but i had to create a one to one relatioship between appointment and User

Django: Accessing extra field in ManytoMany (through=)

I am trying to access the purchaser field in my ManytoMany field. I used through= to add some extra fields. However, it seems I am only able to access the event object, not the extra fields. Can someone explain to me why?
for selected_order in Order.objects.all():
contact_exists = Contact.objects.filter(
email=selected_order.email,
event_related_fields=selected_order.event,
)
if contact_exists:
contact = contact_exists.first()
for x in contact.event_related_fields.all():
print(x.purchaser)
models.py
class Contact(TimeStampedModel):
consent = models.BooleanField(verbose_name=_("Consent"))
email = models.EmailField(verbose_name=_("Your email"))
first_name = models.CharField(
max_length=100, # TODO Length must be same as for billing model
verbose_name=_("First name"),
null=True,
blank=True,
)
last_name = models.CharField(
max_length=100, # TODO Length must be same as for billing model
verbose_name=_("Last name"),
null=True,
blank=True,
)
events = models.ManyToManyField(Event, related_name='contacts')
event_related_fields = models.ManyToManyField(
Event, related_name='event_related_fields', through='EventRelatedFields'
)
organizer = models.ForeignKey(
Organizer, on_delete=models.PROTECT, related_name='contacts'
) # PROTECT = don't allow to delete the organizer if contact exists
class Meta:
verbose_name = _("Contact")
verbose_name_plural = _("Contacts")
ordering = ('created',)
unique_together = ('email', 'organizer')
def __repr__(self):
return "{}: {}".format(self.__class__.__name__, self)
def __str__(self):
return self.email
class EventRelatedFields(TimeStampedModel):
event = models.ForeignKey(Event, on_delete=models.CASCADE)
contact = models.ForeignKey(Contact, on_delete=models.CASCADE)
lead = models.BooleanField(
verbose_name='Lead', default=False
) # Contact who 'Signed Up'
attendee = models.BooleanField(
verbose_name='Attendee', default=False
) # Contact assigned to ticket
purchaser = models.BooleanField(
verbose_name='Purchaser', default=False
) # Contact made the order
class Meta:
unique_together = [['event', 'contact']]
You are defining two many-to-many relationships for some reason, and you've called one of them event_related_fields, with the same related name. As a result you are confusing that with the through table. But since the through table is actually calledEventRelatedFields, you would access its related objects as eventrelatedfields_set.
You should only have one m2m, events:
events = models.ManyToManyField(Event, through='EventRelatedFields', related_name='contacts')
and your class EventRelatedFields should itself set related names:
event = models.ForeignKey(Event, related_name='event_related_fields', on_delete=models.CASCADE)
contact = models.ForeignKey(Contact, related_name='event_related_fields, on_delete=models.CASCADE)
Now you can do:
for x in contact.event_related_fields.all():
print(x.purchaser)

Django: Filter many to many field

I expect to receive a var contact_exists that I can use to update some fields. However, the following query always gives me back django.core.exceptions.FieldError: Related Field got invalid lookup: event
Do you have any idea why event_related_fields__event doesn't work the way I expected?
for selected_order in Order.objects.all():
contact_exists = Contact.objects.filter(
event_related_fields__event=selected_order.event,
)
Here my models.py:
class Contact(TimeStampedModel):
consent = models.BooleanField(verbose_name=_("Consent"))
email = models.EmailField(verbose_name=_("Your email"))
first_name = models.CharField(
max_length=100, # TODO Length must be same as for billing model
verbose_name=_("First name"),
null=True,
blank=True,
)
last_name = models.CharField(
max_length=100, # TODO Length must be same as for billing model
verbose_name=_("Last name"),
null=True,
blank=True,
)
events = models.ManyToManyField(Event, related_name='contacts')
event_related_fields = models.ManyToManyField(
Event, related_name='event_related_fields', through='EventRelatedFields'
)
organizer = models.ForeignKey(
Organizer, on_delete=models.PROTECT, related_name='contacts'
) # PROTECT = don't allow to delete the organizer if contact exists
class Meta:
verbose_name = _("Contact")
verbose_name_plural = _("Contacts")
ordering = ('created',)
unique_together = ('email', 'organizer')
def __repr__(self):
return "{}: {}".format(self.__class__.__name__, self)
def __str__(self):
return self.email
class EventRelatedFields(TimeStampedModel):
event = models.ForeignKey(Event, on_delete=models.CASCADE)
contact = models.ForeignKey(Contact, on_delete=models.CASCADE)
lead = models.BooleanField(
verbose_name='Lead', default=False
) # Contact who 'Signed Up'
attendee = models.BooleanField(
verbose_name='Attendee', default=False
) # Contact assigned to ticket
purchaser = models.BooleanField(
verbose_name='Purchaser', default=False
) # Contact made the order
class Meta:
unique_together = [['event', 'contact']]
You don't need the __event lookup, try using:
for selected_order in Order.objects.all():
contact_exists = Contact.objects.filter(
event_related_fields=selected_order.event,
)
The lookup part should contain field names of Event model.

Django error "add a non-nullable field"

i am getting error django model like this when i try to makemigrations:
You are trying to add a non-nullable field 'person' to owner without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows)
2) Quit, and let me add a default in models.py
i use django 1.8 and this is my model:
class Person(models.Model):
user = models.OneToOneField(User)
alphanumeric = RegexValidator(r'^[0-9a-zA-Z]*$', message='hanya yang mengandung karakter alphanumeric')
email = models.EmailField(verbose_name='email address', unique=True, max_length=244)
username = models.CharField(unique=True, max_length=20, validators=[alphanumeric])
first_name = models.CharField(max_length=30, null=True, blank=True)
last_name = models.CharField(max_length=30, null=True, blank=True)
date_of_birth = models.DateTimeField()
date_joined = models.DateTimeField(auto_now_add=True)
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email']
def get_full_name(self):
fullname = self.first_name+" "+self.last_name
return self.fullname
def get_short_name(self):
return self.username
def list_operator(self):
return self.operators.all()
def __str__(self):
return self.email
class Operator(models.Model):
person = models.ForeignKey(Person, related_name="operators", null=True)
alphanumeric = RegexValidator(r'^[0-9a-zA-Z]*$', message='hanya yang mengandung karakter alphanumeric')
email = models.EmailField(verbose_name='email address', unique=True, max_length=244)
username = models.CharField(unique=True, max_length=20, validators=[alphanumeric])
first_name = models.CharField(max_length=30, null=True, blank=True)
last_name = models.CharField(max_length=30, null=True, blank=True)
date_of_birth = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.username;
i want to know where my code is wrong.
can you help me solved this problem?
Your code isn't wrong. Just follow the instructions provided by the message...
The person field within your Operator model can't be null (because null=True isn't set). You must already have Operators in your database, so Django doesn't know what to do with those.
You need to either: (a) provide a default value in your model, (b) provide a default during the migration process, or (c) enable null values for that field.

Categories