Django: Distinct on forgin key relationship - django

I'm working on a Ticket/Issue-tracker in django where I need to log the status of each ticket. This is a simplification of my models.
class Ticket(models.Model):
assigned_to = ForeignKey(User)
comment = models.TextField(_('comment'), blank=True)
created = models.DateTimeField(_("created at"), auto_now_add=True)
class TicketStatus(models.Model):
STATUS_CHOICES = (
(10, _('Open'),),
(20, _('Other'),),
(30, _('Closed'),),
)
ticket = models.ForeignKey(Ticket, verbose_name=_('ticket'))
user = models.ForeignKey(User, verbose_name=_('user'))
status = models.IntegerField(_('status'), choices=STATUS_CHOICES)
date = models.DateTimeField(_("created at"), auto_now_add=True)
Now, getting the status of a ticket is easy sorting by date and retrieving the first column like this.
ticket = Ticket.objects.get(pk=1)
ticket.ticketstatus_set.order_by('-date')[0].get_status_display()
But then I also want to be able to filter on status in the Admin, and those have to get the status trough a Ticket-queryset, which makes it suddenly more complex. How would I get a queryset with all Tickets with a certain status?

I guess you are trying to avoid a cycle (asking for each ticket status) to filter manually the queryset. As far as I know you cannot avoid that cycle. Here are ideas:
# select_related avoids a lot of hits in the database when enter the cycle
t_status = TicketStatus.objects.select_related('Ticket').filter(status = ID_STATUS)
# this is an array with the result
ticket_array = [ts.ticket for ts in tickets_status]
Or, since you mention you were looking for a QuerySet, this might be what you are looking for
# select_related avoids a lot of hits in the database when enter the cycle
t_status = TicketStatus.objects.select_related('Ticket').filter(status = ID_STATUS)
# this is a QuerySet with the result
tickets = Tickets.objects.filter(pk__in = [ts.ticket.pk for ts in t_status])
However, the problem might be in the way you are modeling the data. What you called TickedStatus is more like TicketStatusLog because you want to keep track of the user and date who change the status.
Therefore, the reasonable approach is to add a field 'current_status' to the Ticket model that is updated each time a new TicketStatus is created. In this way (1) you don't have to order a table each time you ask for a ticket and (2) you would simply do something like Ticket.objects.filter(current_status = ID_STATUS) for what I think you are asking.

Related

Minimizing Flagging System DB Cost

I am trying to figure out a way to make a flagging system that does not require a new instance to be entered in the database (Postgres) every time a user flags a video. (extra context below/above the code) The fields I would like to have are 'Description', 'Timestamp' and 'Flag Choice'.
So I was wondering if this would work. If I make a Flag model and make 'Flag Choices' (Gore, Excessive Violence, ect.) their own Positive Integer Fields and increment the fields accordingly and then combine the id of the post, the description for why they flagged the post, and the timestamp into ONE FIELD by separating new entries by commas into a TextField (In the User Model instead of the Flag model so I know who flagged whatever post)...Will that one Text Field eventually become too big? IMPORTANT: Every time a flag is reviewed and closed, it is deleted from said field (context below)
Context: In the Flag model there will be a post_id field along with Excessive Violence Gore ect. that are Positive Integer fields which are incremented every time someone submits a flag. Then in the User model there will be ONE field which will contain something like the following.
(Commas represent the split of the fields of 'post_id', 'description' and 'timestamp' in the database)
5, "Another flag from the same user in the same TextField.", 2019:9:15
# New Entry
...
Then to get the flag from that one field, I would use a regular expression in combination with a view (that passes a specific video as an argument from a flag management page) to get the post_id, description, timestamp from the TextField (recording the positions for slicing) then after the flag status is "Closed", the function will delete that slice (Starting with the post_id, ending with the timezone, slicing at the commas)
Will this work? The end result SHOULD be... When a post gets flagged, a new Flag model is made, at the same time (if this is the first flag from the user/the first flag for the post)a 'flag_info' field is created in the user model and the post_id, description, and timestamp are entered into said field. If that same user flags another video, a new instance is created for that specific post in the flag model and the flag choice (Gore, Excessive Violence, ect.) is incremented. At the same time the post_id, description, and timestamp are appended to the same field as the following "post_id; description; timestamp," and to grab a specific flag, use a regular expression ( and further processing on the moderation page ) to parse the post_id (used to view the specific post [which will be returned in a different function]) description, and timestamp.
Forgive me if this is difficult to understand, I'm still trying to figure this idea out myself.
I haven't found anything about this through google nor any other search engine.
Flag model
class Flag(models.Model):
FLAG_CHOICES =(
('Sexually Explicit Content', 'Sexually Explicit Content'),
('Child Abuse', 'Child Abuse'), # High priority, auto send to admin, ban if fake flag
('Promotes Definition Terrorism', 'Promotes Definition Terrorism'), # High priority, auto send to admin ban if fake flag
('Gore, Self Harm, Extreme Violence', 'Gore, Self Harm, Extreme Violence'),
('Spam/Misleading/Click-Bait', 'Spam/Misleading/Click-Bait'),
('Calling For Mass Flag', 'Calling For Mass Flag'),
('Doxing', 'Doxing'),
('Animal Abuse', 'Animal Abuse'),
('Threatening Behaviour', 'Threatening Behavior'),
('Calls To Action', 'Calls To Action')
)
STATUS_OPTIONS = (
('Open', 'Open'),
('Being Reviewed', 'Being Reviewed'),
('Pending', 'Pending'),
('Closed', 'Closed'),
)
objects = models.Manager()
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
# Make positive integer fields for flag_choices so we can increment count instead of making a new instance every time
sexually_explicit_content = models.PositiveIntegerField(null=True)
child_abuse = models.PositiveIntegerField(null=True)
promotes_terrorism = models.PositiveIntegerField(null=True)
gore_harm_violence = models.PositiveIntegerField(null=True)
spam_clickbait = models.PositiveIntegerField(null=True)
mass_flag = models.PositiveIntegerField(null=True)
doxing = models.PositiveIntegerField(null=True)
animal_abuse = models.PositiveIntegerField(null=True)
threating_behaviour = models.PositiveIntegerField(null=True)
calls_to_action = models.PositiveIntegerField(null=True)
sexualizing_children = models.PositiveIntegerField(null=True)
# Increment the above fields when a flag with corresponding offence is submitted
who_flagged = models.TextField(null=True) # This will allow me to get all users who flagged a specific post (split by commas, and use a function to loop through the newly formed list of user ids, then on each iteration. I would be able to grab the user model for futher operations
flagged_date = models.DateTimeField(auto_now_add=True, null=True)
flag_choices = models.CharField(choices=FLAG_CHOICES, max_length=100, null=True) # Required Choices of offences
status = models.CharField(choices=STATUS_OPTIONS, default='Open', max_length=50, null=True)
def get_rendered_html(self):
template_name = 'vids/templates/vids/moderation.html'
return render_to_string(template_name, {'object': self.content_object})
User Model or Custom User Profile model
class CustomUser(models.Model):
...
reported = models.TextField() # This will hold all the information about the users flag
# Meaning the following things will be in the same 'box' (
flag_info) in the DB... and will look like this...
" post_id = 4; description = 'There was something in the background against the rules.'; timestamp = 2019:9:25,"
Then when the same user flags another video, something like the following would be appended to the 'flag_info' field...
All of this will be one big long string.
post_id = 24; description = "There was something in the background that showed my email."; timestamp = 2019:10:25,'
# To get flag_info from a user, I would do the following in a view
def get_flag(user, post_id):
# User is going to be the the user model that we need to pull from
# post_id is so I can use regex to pull the slice
# This is really simplified since it would take a while to write the whole thing
info = user.flag_info
split = info.split(",")
for i in split:
if i[0] == post_id:
# do something with it
# Alternatively I could do this
for i in split:
new = i.split(';')
# position 0 is the post_id, position 1 is description and position 3 is timestamp...Here I would do further processsing
To keep track of who flagged what I would make a TextField in the Flag model then every time a user flags a post, their user_id gets recorded in said TextField. When we need to review the flags, I would use the 'get_flag' function after splitting 'who_flagged' by commas. Which would extract the fields I need for processing.
Since I don't have thousands of videos/users, I can't test if the field will eventually become too large.

django querset filter foreign key select first record

I have a History model like below
class History(models.Model):
class Meta:
app_label = 'subscription'
ordering = ['-start_datetime']
subscription = models.ForeignKey(Subscription, related_name='history')
FREE = 'free'
Premium = 'premium'
SUBSCRIPTION_TYPE_CHOICES = ((FREE, 'Free'), (Premium, 'Premium'),)
name = models.CharField(max_length=32, choices=SUBSCRIPTION_TYPE_CHOICES, default=FREE)
start_datetime = models.DateTimeField(db_index=True)
end_datetime = models.DateTimeField(db_index=True, blank=True, null=True)
cancelled_datetime = models.DateTimeField(blank=True, null=True)
Now i have a queryset filtering like below
users = get_user_model().objects.all()
queryset = users.exclude(subscription__history__end_datetime__lt=timezone.now())
The issue is that in the exclude above it is checking end_datetime for all the rows for a particular history object. But i only want to compare it with first row of history object.
Below is how a particular history object looks like. So i want to write a queryset filter which can do datetime comparison on first row only.
You could use a Model Manager method for this. The documentation isn't all that descriptive, but you could do something along the lines of:
class SubscriptionManager(models.Manager):
def my_filter(self):
# You'd want to make this a smaller query most likely
subscriptions = Subscription.objects.all()
results = []
for subscription in subscriptions:
sub_history = subscription.history_set.first()
if sub_history.end_datetime > timezone.now:
results.append(subscription)
return results
class History(models.Model):
subscription = models.ForeignKey(Subscription)
end_datetime = models.DateTimeField(db_index=True, blank=True, null=True)
objects = SubscriptionManager()
Then: queryset = Subscription.objects().my_filter()
Not a copy-pastable answer, but shows the use of Managers. Given the specificity of what you're looking for, I don't think there's a way to get it just via the plain filter() and exclude().
Without knowing what your end goal here is, it's hard to say whether this is feasible, but have you considered adding a property to the subscription model that indicates whatever you're looking for? For example, if you're trying to get everyone who has a subscription that's ending:
class Subscription(models.Model):
#property
def ending(self):
if self.end_datetime > timezone.now:
return True
else:
return False
Then in your code: queryset = users.filter(subscription_ending=True)
I have tried django's all king of expressions(aggregate, query, conditional) but was unable to solve the problem so i went with RawSQL and it solved the problem.
I have used the below SQL to select the first row and then compare the end_datetime
SELECT (end_datetime > %s OR end_datetime IS NULL) AS result
FROM subscription_history
ORDER BY start_datetime DESC
LIMIT 1;
I will select my answer as accepted if not found a solution with queryset filter chaining in next 2 days.

Query latest votes per user per post in Django

I have a voting system where a user can vote up or down on a post. The votes will be used in a calculation, so I need to store them in a log format, ie, I am saving each vote in it's own table.
Something like this:
class PointLog(models.Model):
post = models.ForeignKey(Post, db_index=True)
points = models.IntegerField(db_index=True)
user = models.ForeignKey(User, blank=True, null=True)
time = models.DateTimeField(auto_now_add=True, db_index=True)
data = models.IntegerField() # -1, 0 or 1
Now I need to display 20 Posts, together with the last vote the user did.
I am using django-rest-framework, so I can use a serializer field that looks like this; uservote = serializers.SerializerMethodField(), together with a function like:
def get_uservote(self, obj):
user = self.context['request'].user
vote = PointLog.objects.only('data').filter(user=user, post=obj).last()
return vote.data if vote else 0
But that will do a db-query 1 time per post, which I hope there is a better solution for.
I could save a db-query for each time the get_uservote is ran by saving the queryset in self.context, so that part is covered.
But how can I do a query that based on a list of items returns all the latest data from another table.
A start would be PointLog.objects.filter(user=user, post__in=posts), but what next? Is this even possible using raw SQL in 1 query?
Update 1:
PointLog.objects.filter(...).order_by('post__id').distinct('post__id') would kinda do it, except that I don't think I am guaranteed to get the newest vote. If I use order_by('pk') (or 'time'), I can't use distinct('post__id') as I will get an sql error (ProgrammingError: SELECT DISTINCT ON expressions must match initial ORDER BY expressions)
I found a solution that only added 1 extra query.
The full get_uservote that works would be;
def get_uservote(self, obj):
user = self.context['request'].user
if user.is_authenticated():
if not self.context.get('get_uservote_data'):
self.context['get_uservote_data'] = {i.post_id: i.data for i in PointLog.objects.filter(user=user, post__in=self.instance).order_by('post__id', '-pk').distinct('post__id')}
return self.context['get_uservote_data'].get(obj.id, 0)
else:
return 0 # Return 0 as "not voted" if not logged in
Note that we are doing a couple of tricks here;
Storing the data in self.context['get_uservote_data'] so we only haveto run the query one time.
Using i.post_id as the key for our data, and not i.post.id. Asking for i.post.id here would trigger 1 query per item.
self.instance is the queryset with all the posts we want to filter on.
This all resulted in a query that looks like this:
SELECT DISTINCT ON ("post_pointlog"."post_id") "post_pointlog"."id",
"post_pointlog"."post_id",
"post_pointlog"."data"
FROM "post_pointlog"
WHERE ( "post_pointlog"."user_id" = 20
AND "post_pointlog"."post_id" IN ( 1, 2, 3, 4 ) )
ORDER BY "post_pointlog"."post_id" ASC,
"post_pointlog"."id" DESC

Creating a query with foreign keys and grouping by some data in Django

I thought about my problem for days and i need a fresh view on this.
I am building a small application for a client for his deliveries.
# models.py - Clients app
class ClientPR(models.Model):
title = models.CharField(max_length=5,
choices=TITLE_LIST,
default='mr')
last_name = models.CharField(max_length=65)
first_name = models.CharField(max_length=65, verbose_name='Prénom')
frequency = WeekdayField(default=[]) # Return a CommaSeparatedIntegerField from 0 for Monday to 6 for Sunday...
[...]
# models.py - Delivery app
class Truck(models.Model):
name = models.CharField(max_length=40, verbose_name='Nom')
description = models.CharField(max_length=250, blank=True)
color = models.CharField(max_length=10,
choices=COLORS,
default='green',
unique=True,
verbose_name='Couleur Associée')
class Order(models.Model):
delivery = models.ForeignKey(OrderDelivery, verbose_name='Delivery')
client = models.ForeignKey(ClientPR)
order = models.PositiveSmallIntegerField()
class OrderDelivery(models.Model):
date = models.DateField(default=d.today())
truck = models.ForeignKey(Truck, verbose_name='Camion', unique_for_date="date")
So i was trying to get a query and i got this one :
ClientPR.objects.today().filter(order__delivery__date=date.today())
.order_by('order__delivery__truck', 'order__order')
But, i does not do what i really want.
I want to have a list of Client obj (query sets) group by truck and order by today's delivery order !
The thing is, i want to have EVERY clients for the day even if they are not in the delivery list and with filter, that cannot be it.
I can make a query with OrderDelivery model but i will only get the clients for the delivery, not all of them for the day...
Maybe i will need to do it with a Q object ? or even raw SQL ?
Maybe i have built my models relationships the wrong way ? Or i need to lower what i want to do... Well, for now, i need your help to see the problem with new eyes !
Thanks for those who will take some time to help me.
After some tests, i decided to go with 2 querys for one table.
One from OrderDelivery Queryset for getting a list of clients regroup by Trucks and another one from ClientPR Queryset for all the clients without a delivery set for them.
I that way, no problem !

Ordering by a custom model field in django

I am trying to add an additional custom field to a django model. I have been having quite a hard time figuring out how to do the following, and I will be awarding a 150pt bounty for the first fully correct answer when it becomes available (after it is available -- see as a reference Improving Python/django view code).
I have the following model, with a custom def that returns a video count for each user --
class UserProfile(models.Model):
user = models.ForeignKey(User, unique=True)
positions = models.ManyToManyField('Position', through ='PositionTimestamp', blank=True)
def count(self):
from django.db import connection
cursor = connection.cursor()
cursor.execute(
"""SELECT (
SELECT COUNT(*)
FROM videos_video v
WHERE v.uploaded_by_id = p.id
OR EXISTS (
SELECT NULL
FROM videos_videocredit c
WHERE c.video_id = v.id
AND c.profile_id = p.id
)
) AS Total_credits
FROM userprofile_userprofile p
WHERE p.id = %d"""%(int(self.pk))
)
return int(cursor.fetchone()[0])
I want to be able to order by the count, i.e., UserProfile.objects.order_by('count'). Of course, I can't do that, which is why I'm asking this question.
Previously, I tried adding a custom model Manager, but the problem with that was I also need to be able to filter by various criteria of the UserProfile model: Specifically, I need to be able to do: UserProfile.objects.filter(positions=x).order_by('count'). In addition, I need to stay in the ORM (cannot have a raw sql output) and I do not want to put the filtering logic into the SQL, because there are various filters, and would require several statements.
How exactly would I do this? Thank you.
My reaction is that you're trying to take a bigger bite than you can chew. Break it into bite size pieces by giving yourself more primitives to work with.
You want to create these two pieces separately so you can call on them:
Does this user get credit for this video? return boolean
For how many videos does this user get credit? return int
Then use a combination of #property, model managers, querysets, and methods that make it easiest to express what you need.
For example you might attach the "credit" to the video model taking a user parameter, or the user model taking a video parameter, or a "credit" manager on users which adds a count of videos for which they have credit.
It's not trivial, but shouldn't be too tricky if you work for it.
"couldn't you use something like the "extra" queryset modifier?"
see the docs
I didn't put this in an answer at first because I wasn't sure it would actually work or if it was what you needed - it was more like a nudge in the (hopefully) right direction.
in the docs on that page there is an example
query
Blog.objects.extra(
select={
'entry_count': 'SELECT COUNT(*) FROM blog_entry WHERE blog_entry.blog_id = blog_blog.id'
},
)
resulting sql
SELECT blog_blog.*, (SELECT COUNT(*) FROM blog_entry WHERE blog_entry.blog_id = blog_blog.id) AS entry_count
FROM blog_blog;
Perhaps doing something like that and accessing the user id which you currently have as p.id as appname_userprofile.id
note:
Im just winging it so try to play around a bit.
perhaps use the shell to output the query as sql and see what you are getting.
models:
class Positions(models.Model):
x = models.IntegerField()
class Meta:
db_table = 'xtest_positions'
class UserProfile(models.Model):
user = models.ForeignKey(User, unique=True)
positions = models.ManyToManyField(Positions)
class Meta:
db_table = 'xtest_users'
class Video(models.Model):
usr = models.ForeignKey(UserProfile)
views = models.IntegerField()
class Meta:
db_table = 'xtest_video'
result:
test = UserProfile.objects.annotate(video_views=Sum('video__views')).order_by('video_views')
for t in test:
print t.video_views
doc: https://docs.djangoproject.com/en/dev/topics/db/aggregation/
This is either what you want, or I've completely misunderstood!.. Anywhoo... Hope it helps!