Django M2MFields 'through' widgets - python

Are there exists any examples of Django widgets which can be useful for ManyToManyFields with 'through' attributes? For example, i have these models (got the source from django documentation):
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=128)
def __str__(self): # __unicode__ on Python 2
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
def __str__(self): # __unicode__ on Python 2
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
Obvisously, standart ModelMultipleChoiceField won't work here. I need to populate 'date_joined' , and 'invite_reason' while adding. Which is the simpliest way to achieve this?

This is a bit too complex for a simple widget. I can't even imagine how it would look like. You will have to use inline formsets for that purpose.
This should give something like this:
from django.forms import inlineformset_factory
MembershipFormSet = inlineformset_factory(Group, Membership, fields=(
'person', 'date_joined', 'invite_reason'))
group = Group.objects.get(pk=group_id)
formset = MembershipFormSet(instance=group)
Within django.contrib.admin, you can use inlines with InlineModelAdmin.

Related

Import into tables from Django import_export

I am struggling to populate models in Django by using ForeignKey. Let's say we have as in import_export documentation the following example:
class Author(models.Model):
id = models.BigAutoField(primary_key=True)
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Category(models.Model):
id = models.BigAutoField(primary_key=True)
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Book(models.Model):
name = models.CharField('Book name', max_length=100)
author = models.ForeignKey(Author, blank=True, null=True, )
...
price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
categories = models.ManyToManyField(Category, blank=True)
def __str__(self):
return self.name
How can I implement import_export module that can check if there is an existing author by name (not by id), that is not case sensitive, and that can generate a new author if it does not exist?
As an example, let's say the CSV file looks like:
name,author,...,price,categories
J.R.R. Tolkien,Lord of the Rings,...,40,["cat1","cat2"]
Also, if there is a DateTime field, how to generate that in ForeignKey table?
NOTE: I know about use of natural key:
from import_export.fields import Field
from import_export.widgets import ForeignKeyWidget
class AuthorManager(models.Manager):
def get_by_natural_key(self, name):
return self.get(name=name)
class Author(models.Model):
objects = AuthorManager()
name = models.CharField(max_length=100)
birthday = models.DateTimeField(auto_now_add=True)
def natural_key(self):
return (self.name,)
# Only the author field uses natural foreign keys.
class BookResource(resources.ModelResource):
author = Field(
column_name = "author",
attribute = "author",
widget = ForeignKeyWidget(Author, use_natural_foreign_keys=True)
)
class Meta:
model = Book
But I am not sure how to check for UPPER or lower case in the CSV. And how to generate a new Author if it does not exist.
There are a couple of ways of creating an FK relation during import if it does not already exist.
Option 1 - override the before_import_row() method
class BookResource(resources.ModelResource):
# note use of 'iexact' for case-insensitive lookup
def before_import_row(self, row, **kwargs):
author_name = row["author"]
Author.objects.get_or_create(name__iexact=author_name,
defaults={"name": author_name})
# other code omitted
Option 2 - subclass ForeignKeyWidget
Simply subclass ForeignKeyWidget and implement the check in clean():
class AuthorForeignKeyWidget(widgets.ForeignKeyWidget):
def clean(self, value, row=None, **kwargs):
author, created = Author.objects.get_or_create(name__iexact=value,
defaults={"name": value})
return author
class BookResource(resources.ModelResource):
author = fields.Field(
column_name='author',
attribute='author',
widget=AuthorForeignKeyWidget(Author))
# other code omitted
Either way will work fine. I would personally use option 2.
Also, if there is a DateTime field, how to generate that in ForeignKey table?
Since you are calling Author.objects.get_or_create() you can add a date if you wish, for example:
author, created = Author.objects.get_or_create(name__iexact=value,
defaults={"name": value, "created": timezone.now()})
If using natural keys you can adjust the code as desired.
Related answer about creating in bulk mode

How to make autocomplete_fields search in model's string representation in Django 3

As I am using string representation of a model, it is also shown in a autocomplete_fields (Select2). But the problem is that when i try to search in the field, it is searching the model's name field, not string representation.
Here is my code example:
models.py
class Store(models.Model):
name = models.CharField(max_length=256)
class Department(models.Model):
name = models.CharField(max_length=256)
store = models.ForeignKey(Store, on_delete=models.CASCADE)
class Shelf(models.Model):
name = models.CharField(max_length=256)
department = models.ForeignKey(Department, on_delete=models.CASCADE)
def __string__(self):
return f'{self.department.store.name} {self.department.name} {self.name}'
class Product(models.Model):
name = models.CharField(max_length=256)
shelf = models.ForeignKey(Shelf, on_delete=models.CASCADE)
admin.py
#admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
autocomplete_fields = ('shelf',)
list_display = ('name', 'shelf')
Is it posible to search by model string representation in this case?
First of all your string method should be __str__ instead and since the str method is not a field you cannot add it to the autocomplete field since it looks for a field and works on the DB level.
What you can do is:
#admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
autocomplete_fields = ('shelf__department__store__name','shelf__department__name','name')
list_display = ('name', 'shelf')

Django - manager on ManyToMany relationship

I have 3 models
class Person(models.Model):
name = models.CharField(max_length=128)
class Company(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField (Person, through = 'Membership', related_name = 'companies')
class Membership(models.Model):
person = models.ForeignKey(Person, on_delete=models.CASCADE)
company = models.ForeignKey(Company, on_delete=models.CASCADE)
is_admin = models.BooleanField()
I can then call person.companies.all() to get the list of companies associated with person.
How do I create a manager to have the list of companies associated with person, but whose person is admin (is_admin = True)?
You can create a manager like the following:
managers.py:
from django.db import models
class AdminCompaniesManager(models.Manager):
def get_queryset(self):
return super().get_queryset().companies.filter(membership__is_admin=True)
and then in your Person model (please remind the objects manager):
class Person(models.Model):
name = models.CharField(max_length=128)
objects = models.Manager()
administered_companies = AdminCompaniesManager()
Now you can easily call the following (e.g. in your views):
my_person.administered_companies.all()
PS: a very efficient option (e.g. if you are in a view and you need the list of company ids by a given person) is to query the membership model directly, so you can optimize the query for data retrieval from DB avoiding the joins:
Membership.objects.filter(is_admin=True, person=person).values_list('company_id')
You can filter with:
person.companies.filter(membership__is_admin=True)
This will filter the junction table Membership, such that it will only retrieve Companys for which the Membership has is_admin set to True.
Another option is to retrieve this with:
Company.objects.filter(membership__is_admin=True, members=person)
You can attach this to the Person model with:
class Person(models.Model):
name = models.CharField(max_length=128)
#property
def admin_companies(self):
return self.companies.filter(membership__is_admin=True)

Adding foreignKey widget to django-import-export

I'm trying to import data to one of my models, but it's failing because I'm trying to upload the foreignKey Id, not the iterated number that import-export creates.
models.py
from django.db import models
from import_export import resources
class School(models.Model):
name = models.CharField(max_length=100)
slug = models.CharField(max_length=100)
school_id = models.IntegerField(unique=True)
class Sol(models.Model):
school_id = models.ForeignKey(School, to_field='school_id')
name = models.CharField(max_length = 100)
Al1EOC = models.DecimalField(max_digits=5, decimal_places=2)
AL2EOC = models.DecimalField(max_digits=5, decimal_places=2)
#Class for django-import-export
class SolResource(resources.ModelResource):
class Meta:
model = Sol
My admin.py
from import_export.admin import ImportExportModelAdmin
class SolAdmin(ImportExportModelAdmin):
list_display = ('name', 'school_id')
resources_class = SolResource
pass
admin.site.register(Sol, SolAdmin)
My data.csv
id, name, school_id, Al1EOC, AL2EOC
,'Main st school', 1238008, 12.9, 14.9
When I export the data from the SOL model, I get an iterated number for the school ID. I want the actual School ID - the one that holds the foreignKey relationship. And, subsequently, I need to upload data with that foreignKey number. I know the ForeignKey widget is the way to do it, but I don;t understand how it is implemented.
There is ForeignKeyWidget in the documentation. You can use it here. There are also IntegerWidget and DecimalWidget.
from import_export.admin import ImportExportModelAdmin
class SolResource(resources.ModelResource):
school_id = fields.Field(
column_name='school_id',
attribute='school_id',
widget=ForeignKeyWidget(School, 'name'))
class Meta:
model = Sol
class SolAdmin(ImportExportModelAdmin):
list_display = ('name', 'school_id')
resources_class = SolResource
admin.site.register(Sol, SolAdmin)
This is a working example. Hope it will help.
i think it will help:
class SolResource(resources.ModelResource):
school_id = fields.Field()
class Meta:
# ...
def dehydrate_school_id(self, sol):
return sol.school_id.school_id # You can get all fields of related model. This is example.
Use the widget- works great.
school_id = fields.Field(column_name='school_id', attribute='Sol', widget=widgets.ForeignKeyWidget(Sol, 'school_id'))

Publish date, managers and relations

Consider the following models:
class Artist(GalleryMixin, models.Model):
name = models.CharField(max_length=200)
class Exhibition(GalleryMixin, models.Model):
name = models.CharField(max_length=200, verbose_name=_(u'title'))
artists = models.ManyToManyField('Artist')
Now, I added a publish_on DateTimeField and I don't want to see any objects
on the website with publish_date superior to datetime.datetime.now(), so I
made a manager (using documentation) and it looks like this:
class PublicObjectManager(models.Manager):
use_for_related_fields = True
def get_query_set(self):
qs = super(PublicObjectManager, self).get_query_set()
qs = qs.filter(publish_on__lte=datetime.datetime.now())
return qs
class Artist(GalleryMixin, models.Model):
name = models.CharField(max_length=200)
publish_on = models.DateTimeField(null=True, blank=True)
public_objects = PublicObjectManager()
objects = models.Manager()
class Exhibition(GalleryMixin, models.Model):
name = models.CharField(max_length=200, verbose_name=_(u'title'))
artists = models.ManyToManyField('Artist')
publish_on = models.DateTimeField(null=True, blank=True)
public_objects = PublicObjectManager()
objects = models.Manager()
This works great: non-published objects never appear on the website... But admins are not be able to see non-published objects
! So I could override the admin queryset of course, but it would be hard to
override relations querysets which means that admins could not use Select
fields to create relations to non-published objects.
Ie. admin would not be able to select a non-published Artist in Exhibition.artists.
What's your take on this ?
I've done this by just writing a method on the base object rather than using managers (to me it's more readable than having to remember about 'hidden' base manager changes):
class Exhibition(GalleryMixin, models.Model):
...
def publishedArtists(self):
return self.artist_set.filter(publish_on__lte=datetime.datetime.now())
Then in your template you could just do:
{{exhibition.publishedArtists}}

Categories

Resources