Django manytomany m2m_changed script works, but how? - python

I have a model:
class Course(models.Model):
class Meta:
ordering = ['completion_order']
degree = models.ForeignKey(Degree, on_delete=models.CASCADE)
name = models.CharField(max_length=30, null=False, blank=False)
completion_order = models.PositiveSmallIntegerField(default=0,null=False, blank=False)
required = models.ManyToManyField('Task', default=1, blank=True, symmetrical=False)
And I have written a function to run on "m2m_changed" to automatically adjust the completion order to take into account any changes in prerequisites:
def required_changed(sender, instance, **kwargs):
degreecourses = Courses.objects.all().filter(degree=instance.degree)
for zcr in degreecourses: #resets the order for the all the degree's course instances
zcr.completion_order = 0
for cr in degreecourses: #assigns new completion_order values
if cr.required.count() is 0: # if no requirements sets 'completion_order' to 1
cr.completion_order = 1
else: #sets 'completion_order' to the increment of the highest (max) value of this courses's required course
mx = cr.required.all().aggregate(Max('completion_order'))
neworder = mx['completion_order__max'] + 1
cr.completion_order = neworder
cr.save()
This is run after any change in the m2m thus:
m2m_changed.connect(required_changed, sender=Course.required.through)
I originally set this up iterating through multiple times, but then realised it was unnecessary -my (novice) question is:
Can someone explain how this is able to resolve all of the "required" values in one pass, i.e. without having to iterate through the second loop multiple times?
I'm guessing it's to do with python being object oriented - but I'm new to object oriented programming, so whilst I'm pleased it works, I'm perplexed as to how it can assign the correct running order (i.e. even with many complex interdependencies between multiple courses) with seemingly only one pass?

Related

Trying to get two random samples to have the same matching foreignkey value

I am working on a django app that creates random fantasy character names that pull from the following models:
class VillagerFirstNames(models.Model):
first_name=models.CharField(max_length=30, unique=True)
race = models.ForeignKey(Race, on_delete=models.CASCADE)
def __str__(self):
return self.first_name
class VillagerLastNames(models.Model):
last_name = models.CharField(max_length=30, unique=True)
race = models.ForeignKey(Race, on_delete=models.CASCADE)
def __str__(self):
return self.last_name
My issue is arising in my Views. In order to pull a random.sample I have to convert my query to a list like so:
foreign_first = list(VillagerFirstNames.objects.all()
foreign_first_random = random.sample(foreign_first, 3)
context["foreign_first"] = foreign_first_random
foreign_last = list(VillagerLastNames.objects.filter(race__race=foreign_first_random.race))
context["foreign_last"] = random.sample(foreign_last, 3)
Basically, I want the last names pulled to be of the same race as the ones pulled in the first random sample. I'm having trouble figuring this one out, since the way I'm doing it above takes away the "race" attribute from foreign_first_random.
You can do random selection in Race, from there you can select random VillagerFirstNames and VillagerLastNames. For example:
race = Race.objects.all().order_by('?').first()
race_firstname = race.villagerfirstname_set.all().order_by('?').first()
race_lastname = race.villagerlastname_set.all().order_by('?').first()
Here order_by('?') makes the queryset random.
Update
To pass the values to template, you can try like this:
context["foreign_first"] = race.villagerfirstname_set.order_by('?')[:5]
context["foreign_last"] = race.villagerlastname_set.order_by('?')[:5]

How do you create a function to check the attribute of another field in a separate model?

class Trait(models.Model):
name = models.CharField(max_length=20)
animal_types = models.ManyToManyField(AnimalType)
# slots = models.CharField(default=None, null=True, max_length=4)
#slots is meant to hold a value that determines where it can be placed in animal model
#i.e. null means anywhere, "2" means only trait 2
#def TraitPlace(self):
def __str__(self):
return self.name
#need to add animal details like age and gender to a separate model or integrate/hardcode into animal model as they're only 7 options
class ANIMAL(models.Model):
animal_type = models.ForeignKey(AnimalType, on_delete = models.CASCADE)
first_trait = models.ForeignKey(Trait, on_delete = models.CASCADE)
#first trait and animal required as animals always exist and have at least one trait
# second_trait= models.ForeignKey(Trait, on_delete = models.CASCADE)
# third_trait= models.ForeignKey(Trait, on_delete=models.CASCADE)
I've got an animal model which is used to create an animal with up to 3 traits, the problem is that some traits have specific placement restrictions, i.e. "lithe" can only be in position 3, and glistening can only be in positions 2 and 3. It was suggested to add an additional "slots" field under the trait model.
I understand logically how it's supposed to work out, but am struggling with implementation. I tried to look up documentation and use hasattr under my first_trait but that didn't work or I just didn't have placement correct. Essentially I'm trying to make it so that when the Animal model is called, and a person adds a second trait, for it to go check the "slots" attribute in the database to see if the position of the "trait" is valid. so if slots was "1,2" or "2,3", they would be limited to just being in first and second, and second and third positions, respectively. I spent many hours just trying to make this work and couldn't do it, hoping one of you internet legends can help me out or point me in the right direction. Thanks.
you might want to try adding a model class to validate the data before inserting/updating into Animals & Traits
class AnimalTraits(models.Model):
trait_name = models.ForeignKey(Trait)
animal_type = models.ForeignKey(ANIMAL)
slots = models.CharField(default=None, null=True, max_length=4)
A sql query to figure out what slots are valid for some user inputted animal & trait would look something like this
SELECT at.slots from AnimalTrait at, ANIMAL a, Trait t
WHERE at.animal_type = a.animal_type
AND at.trait_name = t.name
AND a.animal_type = `$user_input_animal_type`
AND t.name = `$user_input_trait`;
& you can add some business logic to determine if the user input slot is in that return value. The nice thing is this can also be used to determine if the trait is valid for the animal

How to create multiple objects with different values at a time in django?

I need to create two models from a single template. Creating Product model is fine. The Product model has the ManyToOne relation with ProductVariant. But I got problem while creating ProductVariant model.
request.POST.getlist('names') this gives me the result like this ['name1','name2] and the same goes for all.
I want to create ProductVariant object with each values. How can I do this ? Also I think there is a problem while stroing a HStoreField. request.POST.getlist('attributes') gives the value like this ['a:b','x:z'] so I converted it into dictionary(but not sure it works).
UPDATE:
What I want is
attributes, names ... all will have the same number of items in the list.
For example if the name is ['a','b','c'] then weight will also have 3 values in the list [12,15,23] like this.
I want to create ProductVariant object 3 times since every list will have 3 items in the list. The first object will have field values from the list first item which is name=a,weight=12.. and for the second object values will be name=b, weight=15 like this.
How will it be possible? Or I should change the logic ? Any suggestions ?
models
class ProductVariant(models.Model):
name = models.CharField(max_length=255, blank=False, null=False)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
attributes = HStoreField()
price = models.FloatField(blank=False, null=False, default=0.0)
views
product = product_form.save()
attributes = request.POST.getlist('attributes')
names = request.POST.getlist('name')
up = request.POST.getlist('price')
weight = request.POST.getlist('weight')
print(names, 'names')
# converting attributes into the dictionary for the HStore field
for attribute in attributes:
attributes_dict = {}
key, value = attribute.split(':')
attributes_dict[key] = value
ProductVariant.objects.create(name=name,...) # for each value I want to create this.
Answer for update:
names = ['a', 'b', 'c']
weights = [12, 15, 23]
params = zip(names, weights)
products = [ProductVariant(name=param[0], weight=param[1]) for param in params]
ProductVariant.objects.bulk_create(products)
I disagree with this approach, but if you really want to do it this way, ziping would be the way as #forkcs pointed out.
I would use Django to help me as much as possible, before i get there, please make this change. float != money
class ProductVariant(models.Model):
name = models.CharField(max_length=255, blank=False, null=False)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
attributes = HStoreField()
price = models.DecimalField(blank=False, null=False, default=0, max_digits=6, decimal_places=2)
Once thats done, the form should look like this:
class ProductVariantForm(forms.ModelForm):
class Meta:
fields = ('name', 'product', 'attributes', 'price')
model = ProductVariant
ProductVariantFormSet = formset_factory(ProductVariantForm)
Note that I don't have to parse/clean/format attributes? Thats because Django did it for me ;)
And you can use it as follow IF you raname your fields and not use the same name multiple times: (instead of all your fields being called "attributes", you call them "form-X-attributes" where X is the number 0-infinity, example)
product = product_form.save()
formset = ProductVariantFormSet(data=request.POST)
if formset.is_valid():
instances = []
for form in formset:
if form.is_valid(): # this could probably be removed
instances.append(form.save())
For extra credit you can also do: (it shouldn't really matter)
product = product_form.save()
formset = ProductVariantFormSet(data=request.POST)
if formset.is_valid():
instances = []
for form in formset:
if form.is_valid(): # this could probably be removed
instances.append(form.save(save=False))
ProductVariant.objects.bulk_create(instances)
What do you gain? STANDARDS!!! AND compartmentalization! Everyone that knows Django knows what you did. All your clean logic will be placed in the right place (the form), and you'll be less error prone.
Ps. i wrote tests for you. https://gist.github.com/kingbuzzman/937a9d207bd937d1b2bb22249ae6bdb2#file-formset_example-py-L142
If you want more information on my approach, see the docs https://docs.djangoproject.com/en/3.1/topics/forms/formsets/
As for attributes, it could be reduced to one line like this:
attributes_dict = dict(map(lambda x: x.split(':'), attributes))
To create multiple objects you should either iterate and create one object at a time or use bulk_create:
for name in names:
ProductVariant.objects.create(name=name,...)
Or
ProductVariant.objects.bulk_create([ProductVariant(name=name) for name in names])
Best practice for this is using bulk_create method.
product_variants = [ProductVariant(name=name) for name in names]
ProductVariant.objects.bulk_create(product_variants)

Python - How to add two model fields in models.py in Django

I have a class in my models.py
class Inventory(models.Model):
date = models.DateField(("Date"), default=datetime.now)
product = models.ForeignKey(Product)
stock_in = models.IntegerField()
stock_out = models.IntegerField()
balance = models.IntegerField()
particulars = models.CharField(max_length=250)
Now I want to add some stocks in the balance. Using the stock_in values to add certain numbers to the balance of a specific product in the Inventory class. Using an UpdateView to it, so that I can just Update the stock_in field then adding that value to the balance.
I'm currently using this, I've tried couple of solution in the internet but to no avail.
#property
def total(self):
return self.stock_in + self.balance
There is no 'official' mechanism in Django to do this. Recently, some ideas of adding some official solution to the Django framework were discussed in this thread on the django-developers mailing list. It might serve as an inspiration for what solution is currently best for your case.
Your method works well for simple calculations. If the property gets more expensive to calculate, using #cached_property can help a bit if the value is used multiple times.
You can also rely on the database to compute these values by adding an annotation to the queryset. This requires defining a custom Manager:
class InventoryManager(models.Manager):
def get_queryset(self):
super().get_queryset().annotate(total=F('stock_in') + F('balance'))
class Inventory(models.Model):
date = models.DateField(("Date"), default=datetime.now)
product = models.ForeignKey(Product)
stock_in = models.IntegerField()
stock_out = models.IntegerField()
balance = models.IntegerField()
particulars = models.CharField(max_length=250)
objects = InventoryManager()
This will add a balance attribute to your Inventory model instances if they are retreived using the default manager.
The problem with this approach (like discussed in the linked django-developers thread) is what your expectations are when modals are changed locally.
For example, with the custom manager in place, if I were to change stock_in for a modal, the value of total would still be valid for the value of stock_in at the time of retrieving it from the database:
>> qs = Inventory.objects.filter(date__gte=date(2017, 12, 22))
>> inventory0 = qs[0]
>> print(inventory0.total, inventory0.stock_in, inventory.balance)
100, 50, 50
>> inventory.balance = 100
>> print(inventory0.total, inventory0.stock_in, inventory.balance)
100, 50, 100
Also, an model instance not fetched from the db at all wont have a total attribute:
>> inventory = Inventory(stock_in=20, balance=10)
>> inventory.total
AttributeError: 'Inventory' object has no attribute 'total'
Adding a __getattr__ method to your class might be a solution to this usecase, but will still result in incorrect answers with local changes.

Auto-increment a field within each category?

I have an auto-increment field called sequence. I only want it to increment by 1 within the topic this model is in. Usually the most would ever be is about 10. Then resets for a new topic. I would like this to be done in a save override method:
class Video(TimeStampedModel):
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200)
description = models.CharField(max_length=500)
topic = models.ForeignKey('video.VideoTopic')
sequence = models.IntegerField(default=1)
def save(self, *args, **kwargs):
# Get last value of sequence and increment by 1
top = Video.objects.order_by('-sequence')[0]
self.sequence = top.sequence + 1
super(Video, self).save()
Problem with my code here is that "top" in the save method will only get the video with the highest numbered sequence.
How do I get the topic of the video being saved from which to filter my query properly?
Get it from self.topic:
top = Video.objects.filter(topic=self.topic).order_by('-sequence')[0]
You can also use latest() instead of sorting and taking [0] out of the queryset:
top = Video.objects.filter(topic=self.topic).latest('sequence')
In this case latest() would get the single Video model instance that has the maximum sequence value for the particular topic.
See also: Overriding predefined model methods.

Categories

Resources