Different ModelField handling - django

I have a scenario where a user need to enter a type of contribution. It can be cash or material. Based on his contribution type, I need to store the cash in IntegerField or material in CharField. How can I do it without making two fields in the model and leaving one always as empty field.
class Contribution(models.Model):
CONTRIBUTION_TYPE_CASH = "cash"
CONTRIBUTION_TYPE_MATERIAL = "material"
CONTRIBUTION_TYPE_CHOICES = [
(CONTRIBUTION_TYPE_CASH, _("cash")),
(CONTRIBUTION_TYPE_MATERIAL, _("material"))
]
contributor = models.ForeignKey(Contributor, related_name="donor", verbose_name=_("contributor"))
type = models.CharField(max_length=20, choices=CONTRIBUTION_TYPE_CHOICES, verbose_name=_("contribution type"))

First variant, keep a single CharField and make sure you properly validate input depending on type. You will have to deal with strings all the time, even if the actual value is a number.
Second variant, use model inheritance and define two different models, one for material contributions and another for cash contributions. You can use an abstract parent in which case you'd have to manually merge the two querysets for getting a global contribution list. Or you could use a concrete parent and use a third party package such as django_polymorphic to seamlessly deal with inherited instances. Either way you'd have to create the apropriate model instance in your backend, even if you use the same dynamic form in your frontend.

Related

Django bulk_create() with models' fields having custom validators

In my Django application, I am using bulk_create(). For one of the fields in a target model I have assigned a set of validators to restrict the allowed value to uppercase letters (alphabets) and to a fixed length of "3", as shown below:
class Plant(models.Model):
plant = models.CharField(primary_key=True, max_length=4, ...
plant_name = models.CharField(max_length=75, ...
plant_short_name = models.CharField(max_length=3, validators=[...
# rest of the fields ...
I am restricting field plant_short_name to something like CHT for say, Plant Charlotte.
Using the source file (.csv) I am able to successfully create new instances using bulk_create, however I find that the data get saved even with field plant_short_name's value being different.
For example, if I use the source as:
plant,plant_name,plant_short_name
9999,XYZ Plant,XY
the new instance still gets created although the length of (string) value of field plant_short_name is only 2 (instead of 3 as defined in the validators).
If I am to use an online create function (say, Django CreateView), the validators work as expected.
How do I control / rstrict the creation of model instance when a field value of incorrect length is used in the source file?
bulk_create():
This method inserts the provided list of objects into the database in
an efficient manner (generally only 1 query, no matter how many
objects there are). Also, does not call save() on each of the
instances, do not send any pre/post_save signals.
By efficient manner it means there is no validation. You can explore more of the function code in django/models/db/query.py inside the environment.

Annotate one part of a range to a new field

So we've been using a DateTimeRangeField in a booking model to denote start and end. The rationale for this might not have been great —separate start and end fields might have been better in hindsight— but we're over a year into this now and there's no going back.
It's generally been fine except I need to annotate just the end datetime onto a related model's query. And I can't work out the syntax.
Here's a little toy example where I want a list of Employees with end of their last booking annotated on.
class Booking(models.Model):
timeframe = DateTimeRangeField()
employee = models.ForeignKey('Employee')
sq = Booking.objects.filter(employee=OuterRef('pk')).values('timeframe')
Employee.objects.annotate(last_on_site=Subquery(sq, output_field=DateTimeField()))
That doesn't work because the annotated value is the range, not the single value. I've tried a heap of modifiers (egs __1 .1 but nothing works).
Is there a way to get just the one value? I guess you could simulate this without the complication of the subquery just doing a simple values lookup. Booking.objects.values('timeframe__start') (or whatever). That's essentially what I'm trying to do here.
Thanks to some help in IRC, it turns out you can use the RangeStartsWith and RangeEndsWith model transform classes directly. These are the things that are normally just registered to provide you with a __startswith filter access to range values, but directly they can pull back the value.
In my example, that means just modifying the annotation slightly:
from django.contrib.postgres.fields.ranges import RangeEndsWith
sq = Booking.objects.filter(employee=OuterRef('pk')).values('timeframe')
Employee.objects.annotate(last_on_site=RangeEndsWith(Subquery(sq[:1])))

Best approach to model contacts in the Django modelling language

I am designing a contact relationship application that needs to store contacts in groups. Basically I have 7 "group types" (simplified it to 3 for my image), each group type shares the same fields so I thought that it would make sense to use an abstract "group", and let all group types inherit the methods from this abstract group.
So this is basically the idea:
However, this approach results in a couple of unexpected difficulties. For example:
I am not able to use a foreignkey of an abstract class, so if I would want to model a relationship between a group and a contact, I have to use the following approach:
limit = (models.Q(app_label='groups', model="Group type A") |
models.Q(app_label='groups', model="Group type B") |
models.Q(app_label='groups', model="Group type C")
)
group_type = models.ForeignKey(ContentType, limit_choices_to=limit)
group_id = models.PositiveIntegerField()
group = GenericForeignKey('group_type', 'group_id')
This seems quite hacky, and with this approach I am forced to do some hard coding as well. I am not able to call all groups with a simple query, maybe a new group will be added in the future.
Is there a better approach to model a relationship like this? Am I using the abstract class completely wrong?
Edit: some extra explanation in response to the questions.
A user is connected to a group with another object called "WorkRelation", because there is some extra data that is relevant when assigning a user to a group (for example his function).
I initially went for an abstract class because I thought that this would give me the flexibility to get all Group types be just calling Group.objects.all(). If I would use a base model, the groups aren't connected and I will also have to hard-code all group names.
Since your child models do not have additional fields, you can make them proxy models of the base group model. Proxy models do not create new database tables, they just allow having different programmatic interfaces over the same table.
You could then define your ForeignKey to the base group model:
group = ForeignKey(BaseGroup)
Use django-polymodels or a similar app to have the groups casted to the right type when queried.
More on model inheritance in the doc.
Why don't use solid base model instead of abstract model? Then you just put contacts as either ForeignKey or ManyToMany to the base model.

Django and writing queries with lots of joins

I have trouble to make these kind of queries with lots of joins. I didn't found examples, but I guess they are not so complicated to write. It's just there are several FKs.
Here is the models.py (not complicated)
class User(AbstractBaseUser, PermissionsMixin): # Django custom user model
# Some stuff
class CliProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL)
class BizProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL)
class Card(models.Model):
linked_client = models.ForeignKey(CliProfile, blank=True, null=True)
class Points(models.Model):
benef_card = models.ForeignKey(Card)
at_owner = models.ForeignKey(BizProfile)
creation_date = models.DateTimeField(auto_now_add=True)
Quick description of the model
a user can be a client (using CliProfile) or a business (using BizProfile)
each card is linked to a client
each card contains a [points - business] association
This way: a client has a card and can has 3 points at Pizza Hut, and 5 points at McDonalds with the same card)
The request I'm trying to write
Functionally speaking, the purpose is a owner (like PizzaHut) can see all his clients (client who have cards which has points at Pizza Hut)
Technically speaking, I'm trying to write a query to get all clients (ie. a CliProfile queryset) whose cards (at least 1 of all) whose points (at least 1 of all) whose owner (there is only 1) whose user (there is only 1) = request.user ?
Do you have any idea how to write such a query? Thanks a lot.
To match fields within models in filter() you need to use two underscores. The following worked for me
CliProfile.objects.filter(card__points__at_owner=request.user)
But #Alex's suggestion makes the most sense unless this was just an example of what you are trying to do.
If you wanted profiles that are associated with one of several cards you can use the __in field lookup:
CliProfile.objects.filter(card__in=IterableOfCards)
Also you don't use == in filter(). That would return True or False and then pass that value in the filter() call effectively making the call filter(True or False) which won't do anything useful. you have to use = because you are passing a named parameter into the filter function.
Why card instead of card_set()?
cart_set only exists within an instance of a CliProfile. You are not in an instance of a CliProfile, you are trying to get a list of them.
You can try it in the terminal and it will tell you the valid choices.
#Note that it doesn't matter what you put after=, since it fails before that is checked.
>>> CliProfile.objects.filter(card_set=True)
FieldError: Cannot resolve keyword 'card_set' into field. Choices are: card, id, user
a CliProfile can be referenced by multiple cards, which is why card_set exists in it but you are trying to match one card. The card whose points at_owner field is request.user.
You would use a_cliprofile_instance.card_set.filter() to get a subset of their cards or a_cliprofile_instance.card_set.all() to display all of their cards

How to sort by annotated Count() in a related model in Django

I'm building a food logging database in Django and I've got a query related problem.
I've set up my models to include (among other things) a Food model connected to the User model through an M2M-field "consumer" via the Consumption model. The Food model describes food dishes and the Consumption model describes a user's consumption of Food (date, amount, etc).
class Food(models.Model):
food_name = models.CharField(max_length=30)
consumer = models.ManyToManyField("User", through=Consumption)
class Consumption(models.Model):
food = models.ForeignKey("Food")
user = models.ForeignKey("User")
I want to create a query that returns all Food objects ordered by the number of times that Food object appears in the Consumption table for that user (the number of times the user has consumed the food).
I'm trying something in the line of:
Food.objects.all().annotate(consumption_times = Count(consumer)).order_by('consumption_times')`
But this will of course count all Consumption objects related to the Food object, not just the ones associated with the user. Do I need to change my models or am I just missing something obvious in the queries?
This is a pretty time-critical operation (among other things, it's used to fill an Autocomplete field in the Frontend) and the Food table has a couple of thousand entries, so I'd rather do the sorting in the database end, rather than doing the brute force method and iterate over the results doing:
Consumption.objects.filter(food=food, user=user).count()
and then using python sort to sort them. I don't think that method would scale very well as the user base increases and I want to design the database as future proof as I can from the start.
Any ideas?
Perhaps something like this?
Food.objects.filter(consumer__user=user)\
.annotate(consumption_times=Count('consumer'))\
.order_by('consumption_times')
I am having a very similar issue. Basically, I know that the SQL query you want is:
SELECT food.*, COUNT(IF(consumption.user_id=123,TRUE,NULL)) AS consumption_times
FROM food LEFT JOIN consumption ON (food.id=consumption.food_id)
ORDER BY consumption_times;
What I wish is that you could mix aggregate functions and F expression, annotate F expressions without an aggregate function, have a richer set of operations/functions for F expressions, and have virtual fields that are basically an automatic F expression annotation. So that you could do:
Food.objects.annotate(consumption_times=Count(If(F('consumer')==user,True,None)))\
.order_by('consumtion_times')
Also, just being able more easily able to add your own complex aggregate functions would be nice, but in the meantime, here's a hack that adds an aggregate function to do this.
from django.db.models import aggregates,sql
class CountIf(sql.aggregates.Count):
sql_template = '%(function)s(IF(%(field)s=%(equals)s,TRUE,NULL))'
sql.aggregates.CountIf = CountIf
consumption_times = aggregates.Count('consumer',equals=user.id)
consumption_times.name = 'CountIf'
rows = Food.objects.annotate(consumption_times=consumption_times)\
.order_by('consumption_times')