Django: save foreign key according to string parameter? - django

Here is the thing.
A branch may contains many products.
A form would post path and branch to product model, in string format.
How could I use like this Product.objects.create(path="path1", branch="branch1") when got the posted data?
or the branch instance must be created in forms.py?
Here is the wrong version: it would raise ValueError: Cannot assign "'branch1'": "Product.branch" must be a "Branch" instance.
class Branch(models.Model):
name = models.CharField(max_length=63, unique=True, blank=True)
class Product(models.Model):
path = models.CharField(max_length=255)
branch = models.ForeignKey(Branch, on_delete=models.CASCADE)
def save(self, *args, **kwargs):
kwargs['branch'], _ = Branch.objects.get_or_create(name=kwargs['branch'])
super(Product, self).save(*args, **kwargs)

This is not the save issue. The error raises during the assignment of string to branch name. If you want to implement the logic, do it before saving
You can use python property to achieve this, with small modifications as shown below. Don't need to override save method.
models.py
class Branch(models.Model):
name = models.CharField(max_length=63, unique=True)
class Product(models.Model):
path = models.CharField(max_length=255)
branch = models.ForeignKey(Branch, on_delete=models.CASCADE)
#property
def branch_name(self):
return self.branch.name
#branch_name.setter
def branch_name(self, value):
self.branch, _ = Branch.objects.get_or_create(name=value)
And your create function should be
Product.objects.create(path="path1", branch_name="branch1")
NOTE : It is branch_name and not branch. Also product.branch remains the branch object and poduct.branch_name returns the name of the branch. This will work with updation also. That is product.branch_name to new value updates the branch of the product

Related

Unique together in models

I have a model called Company.
In a second model which is Branch, I use Company as a foreign key.
class Branch(models.Model):
company = models.ForeignKey(Company, on_delete=models.CASCADE)
Now in some other model, I want to set a property(name) unique together with the Company but I use the branch as a foreign key.
class ABC(models.Model):
name = models.CharField()
branch = models.ForeignKey(Branch, on_delete=models.CASCADE)
class Meta:
unique_together = (
('branch__company', 'name'),
)
Can I do something like the above? It gives me an error that the field is nonexistent. Or can I use both company and branch in my model as foreign key?
class ABC(models.Model):
name = models.CharField()
branch = models.ForeignKey(Branch, on_delete=models.CASCADE)
company = models.ForeignKey(Company, on_delete=models.CASCADE)
class Meta:
unique_together = (
('company', 'name'),
)
I want to attach ABC object with a branch but if once added it should be unique to that company (other branches of that company can not have the same name).
Read about the circular error and was thinking of the same here.
Unique together will be depreciated in the future but I'm not thinking about this right now.
Any advice?
I suggest you to perform validation in the clean method (without a database constraint):
from django.core.exceptions import ValidationError
class ABC(models.Model):
name = models.CharField()
branch = models.ForeignKey(Branch, on_delete=models.CASCADE)
def clean(self):
super().clean()
if ABC.objects.filter(name=self.name, branch__company=self.branch.company).exists():
raise ValidationError('Error message')
def save(self, *args, **kwargs):
# Forces the clean method to be called
self.full_clean()
super().save(*args, **kwargs)

Checking if a field in model is modified and creating instance of another model

I have two models
Project Model
class Project(models.Model):
name = models.CharField(max_length=200)
workflow = models.ForeignKey("WorkflowType", null=True, blank=True, on_delete=models.SET_NULL)
created_on = models.DateTimeField(auto_now_add=True)
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
def __str__(self):
return self.name
Workflow Instance Model
class WorkflowInstance(models.Model):
workflow_step = models.ForeignKey('WorkflowStep', null=True, blank=True, on_delete=models.CASCADE)
project = models.ForeignKey('Project', null=True, blank=True, on_delete=models.SET_NULL)
I want to check if the value of workflow field in "Project" models is added or changed for a particular project.
I am approaching the problem in following manner:
Checking if the previous and the new value of the "workflow" field in a project are different. If yes (modifies), then create the new instance of a project.
#receiver(pre_save, sender=Project)
def projectToBeUpdated(sender, instance, **kwargs):
if instance.id is None:
pass
else:
previous = Project.objects.get(id=instance.id)
if previous.workflow != instance.workflow:
print("workflow value modified. Please create a WorkflowInstance")
Problem: The comparison for previous and new value of the "workflow" field are happening in "pre_save" signal. But my new instance creation for workflowInstance is to be created in "post_save" signal. How can I do this?
Also, ideally I would like to store the previous value of workflow field in "pre_save" and get the new value of the field in "post_save". Reason being, save() method might fail for any reason, while I am comparing the previous and new value in "pre_save" method itself. Making changes in the database without confirming if the save() method executed successfully would be a wrong approach in my view.
You can override the model save method itself and use the following code logic, additionally if you want to identify what fields got modified you can use the dirtyfields package as well.
def save(self, *args, **kwargs):
if not self._state.adding:
changed_attr = self.get_dirty_fields()
else:
is_new = True
super().save(*args, **kwargs)```

Using reverse ForeignKey relation to get count of children for current object not working properly

I'm currently making a Todo app, I have multiple Todolist that can each contain multiple tasks, here's how I made my models :
class Todo(models.Model):
name = models.CharField(max_length=120)
total_tasks = models.IntegerField(default=0)
completed_tasks = models.IntegerField(default=0)
def update_total_tasks(self):
self.total_tasks = self.task_set.all()
def _str_(self):
return self.name
class Task(models.Model):
name = models.CharField(max_length=120)
completed = models.BooleanField(default=False)
todo = models.ForeignKey(Todo, on_delete=models.CASCADE, related_name="tasks")
My todo contains all current tasks as well as completed tasks, the update_total_tasks function is meant to query all tasks linked to that particular todo and update the field accordingly.
This function is called each time a task is created / updated with :
#receiver(models.signals.post_save, sender=Task)
def execute_after_save(sender, instance, created, *args, **kwargs):
instance.todo.update_total_tasks()
The receiver works and calls my update function properly though it seems the query is done the wrong way because I get this error :
AttributeError: 'Todo' object has no attribute 'task_set'
Do you have any idea on why it's not working properly ?
Thanks.
The related_name=… parameter [Django-doc] is set to 'tasks'. Hence you access the relation in reverse with .tasks.
You thus access this with:
class Todo(models.Model):
name = models.CharField(max_length=120)
total_tasks = models.IntegerField(default=0)
completed_tasks = models.IntegerField(default=0)
def update_total_tasks(self):
self.total_tasks = self.tasks.count()

Get all related set objects on Django object including those not yet persisted to database

Let's say I have the following simple Django models:
class Club(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return self.name
class Student(models.Model):
name = models.CharField(max_length=50, unique=True)
club = models.ForeignKey(Club, on_delete=models.CASCADE, null=True, blank=True)
def __str__(self):
return self.name
And I create the following objects:
club1 = Club.objects.create(name="Club1")
student1 = Student.objects.create(name="Student1", club=club1)
print(club1.student_set.all())
# <QuerySet [<Student: Student1>]>
Now I'm going to instantiate a second student object on the club object but NOT YET persist it to the database. Is there any way to get all students associated with club1 BEFORE it has been written to the DB? Looks like using the standard approach just returns the objects stored in the DB:
student2 = Student(name="Student2", club=club1)
print(club1.student_set.all())
# <QuerySet [<Student: Student1>]>
# Goal is to get this:
# <QuerySet [<Student: Student1>, <Student: Student2>]>
The reason I need this is to perform some validation of the staged data state.
If you want to perform validation of the student added, you should use the signal that gets triggered on the save. If you raise an error in the validation, the save does not get performed.
from django.db.models.signals import m2m_changed
m2m_changed.connect(student_set_changed, sender=Club.student_set.through)
def student_set_changed(*args, **kwargs):
# Do you validation
return
More on how to handle Many2Many field changes in Django Docs
You can add related_name attr in order to have easy access to all club's students. And you can validate students' quantity before saving it.
class Student(models.Model):
name = models.CharField(max_length=50, unique=True)
club = models.ForeignKey(
Club,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='students', # add this related name and run migratioin
)
def __str__(self):
return self.name
def validate_club_students(self):
if self.club.students.count() >= 3:
raise ValidationError('The club already has 3 students!')
def save(self, *arg, **kwargs):
self.validate_club_students()
super().save(*arg, **kwargs)
It means that before saving a new student for a certain club you can check how many students does the club have.

Django: How to check a Form with a m2m relation object already exists or is “unique_together”?

I am testing forms and nesting models in django. In my Project a Person can enter departure, arrival (city names) and choose a weekly day (Mon-Fri). Maybe he drives every “Tuesday” from Amsterdam to Paris. I wanted this constellation to be unique – just for fun. So If another user enters the same route the relation should be linked to the same Car.object.
Models.py
class Person(models.Model):
name = models.CharField(max_length=255, blank=False, unique=True)
route = models.ManyToManyField('Car')
def __str__(self):
return self.name
class Car(models.Model):
name = models.CharField(max_length=255, blank=False, unique=True)
weekdays = models.ForeignKey('Week', null=True, blank=False, on_delete=models.SET_NULL)
departure = models.CharField(max_length=255, blank=False)
arrival = models.CharField(max_length=255, blank=False)
class Meta:
unique_together = ['weekdays', 'departure', 'arrival'] # --- Unique combination
def __str__(self):
return self.name
class Week(models.Model):
day = models.CharField(max_length=255, blank=False, unique=True)
def __str__(self):
return self.day
views.py
class RouteCreateView(CreateView):
model = Person
template_name ="testa/create_route.html"
form_class = RouteForm
success_url = reverse_lazy('testa:testa_home')
def form_valid(self, form):
return super().form_valid(form)
forms.py
class RouteForm(forms.ModelForm):
# --- apply ChoiceField
day = forms.ModelChoiceField(queryset=None)
car_name = forms.CharField()
departure = forms.CharField()
arrival = forms.CharField()
class Meta:
model = Person
fields = [
'name'
]
def __init__(self, *args, **kwargs):
super(RouteForm, self).__init__(*args, **kwargs)
self.fields['day'].queryset = Week.objects.all()
def save(self, commit=True):
personData = super().save(commit)
data = self.cleaned_data
carData = Car(name=data['car_name'], weekdays=data['day'], departure=data['departure'], arrival=data['arrival'])
if commit:
carData.save()
personData.route.add(carData) # --- save m2m relation
return personData
If i enter two times for example „“Tuesday” from Amsterdam to Paris “ then an Error Message appears obviously, this error message (it´s german), telling me I have a double entry / Key.
Question
So my save()Method does not work because I need some kind of logic, so that Django takes the existing car.object or creates a new - if it is not a double entry. But I do not know where to start? The easiest way would be to get some kind of response from my model meta option Car.unique_together so "if it´s an “double-key error” then take the existing object". Is there a way to fetch the response? And what kind of Values it would be, only errors, could not find any hint in the doc? Or should I try some logic with exists()
That was my kind of idea / approach of a new save() 😊
def save(self, commit=True):
personData = super().save(commit)
data = self.cleaned_data
carData = Car(name=data['car_name'], weekdays=data['day'], departure=data['departure'], arrival=data['arrival'])
if commit:
# Check if database sends unique_together response
# if yes
if Car.Meta.unique_together is True:
getAlternative = Car.object.get(Meta.unique_together) # --- get the object which already exist
personData.route.add(getAlternative) # --- save m2m relation
# if not
else:
carData.save() # --- save object
personData.route.add(carData) # --- save m2m relation
return personData
obviously i get a error message: type object 'Car' has no attribute
'Meta'
Theres get_or_create for such use case: https://docs.djangoproject.com/en/2.2/ref/models/querysets/#get-or-create
...
car, created = Car.objects.get_or_create(
weekdays=data['day'],
departure=data['departure'],
arrival=data['arrival'],
defaults = dict(name=data['car_name']),
)
personData.route.add(car)
...
Obviously given name gets ignored if another car with same weekdas, departure, arrival has been found.
I suggest to put the code for creating the car and adding the route in a transaction.atomic() https://docs.djangoproject.com/en/2.2/topics/db/transactions/#django.db.transaction.atomic