Import-Export with "multilevel" models - django

I am figuring how can I manage this situation with django-import-export using the same excel and differents models with differents djangoapps. I have the following models:
# employees/models.py
class Employee(models.Model):
name = models.CharField(max_length=100)
job = models.ForeignKey(Job, on_delete=models.SET_NULL, null=True, blank=True)
# jobs/models.py
class Job(models.Model):
value = models.CharField(max_length=100)
department = models.ForeignKey(Department, on_delete=models.SET_NULL, null=True, blank=True)
place = models.ForeignKey(Place, on_delete=models.SET_NULL, null=True, blank=True)
class Department(models.Model):
value = models.CharField(max_length=100)
business = models.ForeignKey(Business, on_delete=models.SET_NULL, null=True, blank=True)
class Place(models.Model):
value = models.CharField(max_length=100)
business = models.ForeignKey(Business, on_delete=models.SET_NULL, null=True, blank=True)
class Business(models.Model):
name = models.CharField(max_length=100)
On excel I have following values:
xls_employee_name, xls_employee_job, xls_business_name
Jon Doe, Web Developer, Company 1
I know how to import employee name and his job because Job is directly related by ForeignKey. But how can I import business name?
Below is the code for employee name and his job:
# employees/resource.py
class EmpleadoResource(resources.ModelResource):
name = fields.Field(attribute='nombre', column_name='Nombre')
job = fields.Field(
column_name='xls_employee_job',
attribute='job',
widget=ForeignKeyWidget(
Job,
field='value'
))
class Meta:
model = Employee
fields = ('name','job',)
skip_unchanged = True
def before_import_row(self, row, **kwargs):
self.job = row["xls_employee_job"]
def after_import_instance(self, instance, new, row_number=None, **kwargs):
Job.objects.get_or_create(name=self.job)
Is it possible to import business name using one excel? I appreciate if someone could guide me. I'm also pretty new with django.

thank you for all your answers. I finally manage it and this is the solution(it was a little trickier for me, but very simple. I tried to translate all spanish names to english, sorry if I misslooked some):
# employees/resource.py
class EmployeeResource(resources.ModelResource):
name = fields.Field(attribute='name', column_name='Name')
job = fields.Field(
column_name='xls_employee_job',
attribute='job',
widget=ForeignKeyWidget(
Job,
field='value'
))
place = fields.Field(attribute='place', column_name='xls_place_column')
department = fields.Field(attribute='department', column_name='xls_department_column')
business = fields.Field(attribute='business', column_name='xls_business_name')
class Meta:
model = Employee
fields = ('name','job','place','department','business')
skip_unchanged = True
def before_import_row(self, row, **kwargs):
self.job = row["xls_employee_job"]
self.place = row["xls_place_column"]
self.department = row["xls_department_column"]
self.business = row["xls_business_name"]
def after_import_instance(self, instance, new, row_number=None, **kwargs):
Job.objects.get_or_create(name=self.job)
Centro.objects.get_or_create(name=self.centro)
Departamento.objects.get_or_create(name=self.departamento)
Empresa.objects.get_or_create(nombre=self.empresa)
I was stucked using widgets for business, department and place, but it was not necessary

Option 1
You can assign the column value for each row to a temporary attribute on the model (it doesn't matter that this attribute is not in the Employee model):
def before_import_row(self, row, **kwargs):
self.job = row["xls_employee_job"]
# assign row value to a temporary attr
self.business = row["xls_business_name"]
def after_import_instance(self, instance, new, row_number=None, **kwargs):
Job.objects.get_or_create(name=self.job)
# create instance using temp attr value
Business.objects.get_or_create(name=self.business)
Option 2
You can create all Business instances by processing the dataset in a batch before processing the Employee resources:
def before_import(self, dataset, using_transactions, dry_run, **kwargs):
for row in dataset.dict:
business = row["xls_business_name"]
Business.objects.create(name=business)
Note that you could use bulk_create() here to make this process more efficient.
Add transaction protection as you see fit, and bear in mind that the Business and Job entities will be created even if your import fails.

Related

Django filter two levels of DB relationships

I have three models that I'm trying to hook up so I can pull-out StudentItem information based upon ID of the Course that was passed in.
The models look like the following:
class Student(models.Model):
first_name = models.CharField(max_length=128, unique=False)
last_name = models.CharField(max_length=128, unique=False)
class Course(models.Model):
name = models.CharField(max_length=256, unique=False)
course_student = models.ManyToManyField(Student)
class StudentItem(models.Model):
item_student = models.ForeignKey('Student',on_delete=models.CASCADE)
description = models.CharField(max_length=256, unique=False, blank=True)
Right now, my view looks like:
class CourseDetailView(LoginRequiredMixin,DetailView):
model = models.Course
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['student_item_list'] = StudentItem.objects.prefetch_related(Prefetch('item_student__id__course_student',queryset=Course.objects.filter(pk=self.kwargs['pk'])))
return context
I can get everything connected via prefetch across all of the models but the queryset filtering is not working. I am getting all records for every course regardless of the course ID that was passed in.
Hopefully it's a small tweak. Really appreciate the help!

Create object if not exists in Django ImportExportModelAdmin

I have these two models:
Profile_model.py
class Profile(models.Model):
firstname = models.CharField(max_length=200, blank=False)
lastname = models.CharField(max_length=200, blank=False)
email = models.CharField(max_length=200, unique=True, blank=False)
...
Investment_model.py
class Investment(models.Model):
invested = models.DecimalField(max_digits=9, decimal_places=2, blank=True, null=True)
profile = models.ForeignKey(Profile, on_delete=models.CASCADE)
...
and I have this admin:
Investment_admin.py
class InvestmentResource(resources.ModelResource):
...
firstname = fields.Field(attribute='profile',
widget=ForeignKeyWidget(Profile, field='firstname'),
column_name='firstname')
lastname = fields.Field(attribute='profile',
widget=ForeignKeyWidget(Profile, field='lastname'),
column_name='lastname')
email = fields.Field(attribute='email',
widget=ForeignKeyWidget(Profile, field='email'),
column_name='email')
class Meta:
model = Investment
fields = (
'firstname',
'lastname',
'email',
'invested',)
export_order = fields
class InvestmentAdmin(ImportExportModelAdmin, admin.ModelAdmin):
...
resource_class = InvestmentResource
...
I am using django's ImportExportModelAdmin for bulk imports and exports but when I try to import, I get this error:
I get that its producing this error because the profile hasn't been created yet. But what do I have to do to implement an update_or_create inside the ImportExportModelAdmin?
Option 1 is to use before_import() to scan through the dataset and create Profiles in batch if they do not exist already.
Option 2 is to override methods and create the profiles just before the Investment row is imported. This is only necessary for new Investment objects. This assumes that 'email' will uniquely identify a Profile, you will need to adjust this if not.
Note that firstname and lastname can be set on the Profile object before it is created.
class InvestmentResource(resources.ModelResource):
firstname = fields.Field(attribute='profile__firstname',
widget=CharWidget(), column_name='firstname')
lastname = fields.Field(attribute='profile__lastname',
widget=CharWidget(), column_name='lastname')
email = fields.Field(attribute='email',
widget=ForeignKeyWidget(Profile, field='email'),
column_name='email')
def before_import_row(self, row, row_number=None, **kwargs):
self.email = row["email"]
def after_import_instance(self, instance, new, row_number=None, **kwargs):
"""
Create any missing Profile entries prior to importing rows.
"""
if (
new
and not Profile.objects.filter(
name=self.email
).exists()
):
obj, created = Profile.objects.get_or_create(
name=self.email
)
if created:
logger.debug(f"no Profile in db with name='{self.email}' - created")
instance.profile = obj
Obviously the Profile creation will be a side-effect of an import, so you may need to consider using transactions if you don't want Profiles created if the import fails.

Linking two models automatically in django

I would like when i create new order to be linked to this company. now, i have to choose it manually
class Company(models.Model):
name = models.CharField(max_length=64)
address = models.TextField(max_length=250)
class Order(models.Model):
company = models.ForeignKey('Company', on_delete=models.CASCADE)
order_date = models.CharField(max_length=64)
order_notes = models.TextField(max_length=250)
First of all if every Order is connected to this particular Company creating Foreign Key is overintended. If you still want to do this for some reason here is the solution.
class Order(models.Model):
company = models.ForeignKey('Company', on_delete=models.CASCADE)
order_date = models.CharField(max_length=64)
order_notes = models.TextField(max_length=250)
def save(self, *args, **kwargs):
# u have to have a new order in db
super().save(*args, **kwargs)
# then assing this particular company
self.company = Company.objects.get(name='the_company_name', address='the_company_address')
# and again save the changes
super().save(*args, **kwargs)
but if you want to do this this way consider making the Company name and address unique_together

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.

Filtering Django models by user & object

I'm learning Django with a dummy example but having difficulty in understanding how to correctly filter my Django models by an authorised user in my views.
In my view I want to list the transactions associated with a users portfolio. The code below runs but when trying to access the result of 't' I get the error:
'ValueError: The QuerySet value for an exact lookup must be limited to one result using slicing.'
Any help would be much appreciated, thanks.
if request.user.is_authenticated:
# Get model data
pf = Portfolio.objects.filter(user=request.user)
t = Transaction.objects.filter(pf=pf)
My model is as below:
from django.db import models
from django.contrib.auth.models import User
class Portfolio(models.Model):
# Portfolio has one user associated with it
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=100, default='-')
def __str__(self):
return self.name
class Transaction(models.Model):
# Transaction has one equity associated with it
equity = models.ForeignKey('Equity', on_delete=models.CASCADE, null=True)
# Transaction has one portfolio associated with it
pf = models.ForeignKey('Portfolio', on_delete=models.CASCADE)
BUY = 'BUY'
SELL = 'SELL'
BUY_OR_SELL = (
(BUY, 'BUY'),
(SELL, 'SELL'),
)
action = models.CharField(choices=BUY_OR_SELL, default=BUY, max_length=5)
num = models.FloatField(default=1)
price = models.FloatField(default=0)
date = models.DateField('date')
fee = models.FloatField(default=0)
def __str__(self):
return f'{self.equity}, {self.num}x{self.price}, {self.date:%d %b %Y}'
class Equity(models.Model):
class Meta:
verbose_name_plural = "Equities"
CUR_EUR = 'EUR'
CUR_GBP = 'GBP'
CUR_USD = 'USD'
CURRENCY_CHOICES = (
(CUR_EUR, 'EUR'),
(CUR_GBP, 'GBP'),
(CUR_USD, 'USD'),
)
symbol = models.CharField(max_length=20, default='-')
exchange = models.CharField(max_length=100, default='-')
currency = models.CharField(max_length=15, choices=CURRENCY_CHOICES, default=CUR_USD)
def __str__(self):
return self.symbol
Many thanks!
pf is here a collection of Portfolio objects, so you can query it with the __in lookup [Django-doc]:
Transaction.objects.filter(pf__in=pf)
Or if you are not interested in the Porfolio objects itself, you can make a query like:
Transaction.objects.filter(pf__user=request.user)
The query below will result in a query like:
SELECT transaction.*
FROM transaction
JOIN portfolio ON transaction.pf_id = portfolio.id
WHERE porfolio.user_id = 123
(with 123 the id of the request.user)